Skip to content

Commit a23ccde

Browse files
mattlewis92mgechev
authored andcommitted
feat(component-change-detection): add change detection strategy rule (#737)
Closes #135
1 parent a75c204 commit a23ccde

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

src/componentChangeDetectionRule.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Utils } from 'tslint/lib';
2+
import * as Lint from 'tslint';
3+
import * as ts from 'typescript';
4+
import { getDecoratorArgument, getDecoratorName } from './util/utils';
5+
import { sprintf } from 'sprintf-js';
6+
import { NgWalker } from './angular';
7+
8+
export class Rule extends Lint.Rules.AbstractRule {
9+
public static metadata: Lint.IRuleMetadata = {
10+
ruleName: 'component-change-detection',
11+
type: 'functionality',
12+
description: 'Enforce the preferred component change detection type as ChangeDetectionStrategy.OnPush.',
13+
descriptionDetails: Utils.dedent`
14+
See more at https://angular.io/api/core/ChangeDetectionStrategy
15+
`,
16+
options: null,
17+
optionsDescription: 'Not configurable.',
18+
rationale: Utils.dedent`
19+
By using OnPush for change detection, Angular will only run a change detection cycle when that
20+
components inputs or outputs change
21+
`,
22+
typescriptOnly: true
23+
};
24+
25+
static CHANGE_DETECTION_INVALID_FAILURE =
26+
'The changeDetection value of the component "%s" should be set to ChangeDetectionStrategy.OnPush';
27+
28+
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
29+
return this.applyWithWalker(new ComponentChangeDetectionValidatorWalker(sourceFile, this));
30+
}
31+
}
32+
33+
export class ComponentChangeDetectionValidatorWalker extends NgWalker {
34+
constructor(sourceFile: ts.SourceFile, private rule: Rule) {
35+
super(sourceFile, rule.getOptions());
36+
}
37+
38+
visitClassDeclaration(node: ts.ClassDeclaration) {
39+
ts.createNodeArray(node.decorators).forEach(this.validateDecorator.bind(this, node.name!.text));
40+
super.visitClassDeclaration(node);
41+
}
42+
43+
private validateDecorator(className: string, decorator: ts.Decorator) {
44+
const argument = getDecoratorArgument(decorator)!;
45+
const name = getDecoratorName(decorator);
46+
47+
// Run only for Components
48+
if (name === 'Component') {
49+
this.validateComponentChangeDetection(className, decorator, argument);
50+
}
51+
}
52+
53+
private validateComponentChangeDetection(className: string, decorator: ts.Decorator, arg: ts.Node) {
54+
if (!ts.isObjectLiteralExpression(arg)) {
55+
return;
56+
}
57+
58+
const changeDetectionAssignment = arg.properties
59+
.filter(prop => ts.isPropertyAssignment(prop) && this.validateProperty(prop))
60+
.map(prop => (ts.isPropertyAssignment(prop) ? prop.initializer : undefined))
61+
.filter(Boolean)[0] as ts.PropertyAccessExpression;
62+
63+
if (!changeDetectionAssignment) {
64+
this.addFailureAtNode(decorator, sprintf(Rule.CHANGE_DETECTION_INVALID_FAILURE, className));
65+
} else {
66+
if (!this.validateChangeDetectionType(changeDetectionAssignment!.name.escapedText as string)) {
67+
this.addFailureAtNode(changeDetectionAssignment, sprintf(Rule.CHANGE_DETECTION_INVALID_FAILURE, className));
68+
}
69+
}
70+
}
71+
72+
private validateProperty(p: ts.PropertyAssignment): boolean {
73+
return ts.isIdentifier(p.name) && p.name.text === 'changeDetection';
74+
}
75+
76+
private validateChangeDetectionType(changeDetectionValue: string): boolean {
77+
return changeDetectionValue === 'OnPush';
78+
}
79+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export { Rule as UsePipeDecoratorRule } from './usePipeDecoratorRule';
3939
export { Rule as UsePipeTransformInterfaceRule } from './usePipeTransformInterfaceRule';
4040
export { Rule as UseViewEncapsulationRule } from './useViewEncapsulationRule';
4141
export { Rule as RelativePathExternalResourcesRule } from './relativeUrlPrefixRule';
42+
export { Rule as ComponentChangeDetectionRule } from './componentChangeDetectionRule';
4243

4344
export * from './angular';
4445

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { assertSuccess, assertAnnotated } from './testHelper';
2+
3+
describe('component-change-detection', () => {
4+
describe('invalid component change detection', () => {
5+
it('should fail when component used without preferred change detection type', () => {
6+
let source = `
7+
@Component({
8+
changeDetection: ChangeDetectionStrategy.Default
9+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10+
})
11+
class Test {}
12+
`;
13+
assertAnnotated({
14+
ruleName: 'component-change-detection',
15+
message: 'The changeDetection value of the component "Test" should be set to ChangeDetectionStrategy.OnPush',
16+
source
17+
});
18+
});
19+
20+
it('should fail when component change detection is not set', () => {
21+
let source = `
22+
@Component({
23+
~~~~~~~~~~~~
24+
selector: 'foo'
25+
})
26+
~~
27+
class Test {}`;
28+
assertAnnotated({
29+
ruleName: 'component-change-detection',
30+
message: 'The changeDetection value of the component "Test" should be set to ChangeDetectionStrategy.OnPush',
31+
source
32+
});
33+
});
34+
});
35+
36+
describe('valid component selector', () => {
37+
it('should succeed when a valid change detection strategy is set on @Component', () => {
38+
let source = `
39+
@Component({
40+
changeDetection: ChangeDetectionStrategy.OnPush
41+
})
42+
class Test {}
43+
`;
44+
assertSuccess('component-change-detection', source);
45+
});
46+
});
47+
});

0 commit comments

Comments
 (0)