Skip to content

Commit 2074d02

Browse files
rafaelss95mgechev
authored andcommitted
refactor(rule): rename/clean up 'component-change-detection' (#782)
1 parent a9c4ae9 commit 2074d02

7 files changed

+126
-139
lines changed

src/componentChangeDetectionRule.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { Rule as AngularWhitespaceRule } from './angularWhitespaceRule';
2-
export { Rule as ComponentChangeDetectionRule } from './componentChangeDetectionRule';
32
export { Rule as ComponentClassSuffixRule } from './componentClassSuffixRule';
43
export { Rule as ComponentSelectorRule } from './componentSelectorRule';
54
export { Rule as ContextualDecoratorRule } from './contextualDecoratorRule';
@@ -25,6 +24,7 @@ export { Rule as NoQueriesMetadataPropertyRule } from './noQueriesMetadataProper
2524
export { Rule as NoUnusedCssRule } from './noUnusedCssRule';
2625
export { Rule as PipePrefixRule } from './pipePrefixRule';
2726
export { Rule as PreferInlineDecoratorRule } from './preferInlineDecoratorRule';
27+
export { Rule as PreferOnPushComponentChangeDetectionRule } from './preferOnPushComponentChangeDetectionRule';
2828
export { Rule as PreferOutputReadonlyRule } from './preferOutputReadonlyRule';
2929
export { Rule as RelativeUrlPrefixRule } from './relativeUrlPrefixRule';
3030
export { Rule as TemplateAccessibilityAltTextRule } from './templateAccessibilityAltTextRule';
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { IRuleMetadata, RuleFailure } from 'tslint/lib';
2+
import { AbstractRule } from 'tslint/lib/rules';
3+
import { dedent } from 'tslint/lib/utils';
4+
import { isPropertyAccessExpression, SourceFile } from 'typescript/lib/typescript';
5+
import { ComponentMetadata } from './angular/metadata';
6+
import { NgWalker } from './angular/ngWalker';
7+
import { getDecoratorPropertyInitializer } from './util/utils';
8+
9+
const ON_PUSH = 'OnPush';
10+
11+
export class Rule extends AbstractRule {
12+
static readonly metadata: IRuleMetadata = {
13+
description: `Enforces component's change detection to ChangeDetectionStrategy.${ON_PUSH}.`,
14+
options: null,
15+
optionsDescription: 'Not configurable.',
16+
rationale: dedent`
17+
By default Angular uses the ChangeDetectionStrategy.Default.
18+
19+
This strategy doesn’t assume anything about the application, therefore every time something changes in our application, as a result of various user events, timers, XHR, promises, etc., a change detection will run on all components.
20+
21+
By using ChangeDetectionStrategy.${ON_PUSH}, Angular will only run the change detection cycle in the following cases:
22+
* Inputs references change.
23+
* An event originated from the component or one of its children.
24+
* If manually called.
25+
`,
26+
ruleName: 'prefer-on-push-component-change-detection',
27+
type: 'functionality',
28+
typescriptOnly: true
29+
};
30+
31+
static readonly FAILURE_STRING = `The changeDetection value of a component should be set to ChangeDetectionStrategy.${ON_PUSH}`;
32+
33+
apply(sourceFile: SourceFile): RuleFailure[] {
34+
return this.applyWithWalker(new PreferOnPushComponentChangeDetectionWalker(sourceFile, this.getOptions()));
35+
}
36+
}
37+
38+
class PreferOnPushComponentChangeDetectionWalker extends NgWalker {
39+
protected visitNgComponent(metadata: ComponentMetadata): void {
40+
this.validateComponent(metadata);
41+
super.visitNgComponent(metadata);
42+
}
43+
44+
private validateComponent(metadata: ComponentMetadata): void {
45+
const { decorator: metadataDecorator } = metadata;
46+
const changeDetectionExpression = getDecoratorPropertyInitializer(metadataDecorator, 'changeDetection');
47+
48+
if (!changeDetectionExpression) {
49+
this.addFailureAtNode(metadataDecorator, Rule.FAILURE_STRING);
50+
} else if (isPropertyAccessExpression(changeDetectionExpression) && changeDetectionExpression.name.text !== ON_PUSH) {
51+
this.addFailureAtNode(changeDetectionExpression, Rule.FAILURE_STRING);
52+
}
53+
}
54+
}
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
1+
import { IRuleMetadata, RuleFailure } from 'tslint/lib';
22
import { AbstractRule } from 'tslint/lib/rules';
33
import { isPropertyAccessExpression, SourceFile } from 'typescript/lib/typescript';
44
import { ComponentMetadata } from './angular/metadata';
55
import { NgWalker } from './angular/ngWalker';
66
import { getDecoratorPropertyInitializer } from './util/utils';
77

8+
const NONE = 'None';
9+
810
export class Rule extends AbstractRule {
911
static readonly metadata: IRuleMetadata = {
10-
description: 'Disallows using of ViewEncapsulation.None.',
12+
description: `Disallows using ViewEncapsulation.${NONE}.`,
1113
options: null,
1214
optionsDescription: 'Not configurable.',
1315
ruleName: 'use-component-view-encapsulation',
1416
type: 'maintainability',
1517
typescriptOnly: true
1618
};
1719

18-
static readonly FAILURE_STRING = 'Using "ViewEncapsulation.None" makes your styles global, which may have an unintended effect';
20+
static readonly FAILURE_STRING = `Using ViewEncapsulation.${NONE} makes your styles global, which may have an unintended effect`;
1921

2022
apply(sourceFile: SourceFile): RuleFailure[] {
2123
return this.applyWithWalker(new ViewEncapsulationWalker(sourceFile, this.getOptions()));
@@ -29,12 +31,12 @@ class ViewEncapsulationWalker extends NgWalker {
2931
}
3032

3133
private validateComponent(metadata: ComponentMetadata): void {
32-
const encapsulation = getDecoratorPropertyInitializer(metadata.decorator, 'encapsulation');
34+
const encapsulationExpression = getDecoratorPropertyInitializer(metadata.decorator, 'encapsulation');
3335

34-
if (!encapsulation || (isPropertyAccessExpression(encapsulation) && encapsulation.name.text !== 'None')) {
36+
if (!encapsulationExpression || (isPropertyAccessExpression(encapsulationExpression) && encapsulationExpression.name.text !== NONE)) {
3537
return;
3638
}
3739

38-
this.addFailureAtNode(encapsulation, Rule.FAILURE_STRING);
40+
this.addFailureAtNode(encapsulationExpression, Rule.FAILURE_STRING);
3941
}
4042
}

src/util/utils.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,16 @@ export const getDecoratorArgument = (decorator: Decorator): ObjectLiteralExpress
105105
};
106106

107107
export const getDecoratorPropertyInitializer = (decorator: Decorator, name: string): Expression | undefined => {
108-
const args = isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
109-
const properties = createNodeArray(args && isObjectLiteralExpression(args) ? args.properties : undefined);
108+
const args = getDecoratorArgument(decorator);
110109

111-
return properties
112-
.filter(prop => prop.name && isIdentifier(prop.name) && prop.name.text === name)
113-
.map(prop => (isPropertyAssignment(prop) ? prop.initializer : undefined))
114-
.pop();
110+
if (!args || !isObjectLiteralExpression(args)) return undefined;
111+
112+
const properties = createNodeArray(args.properties);
113+
const property = properties.find(prop => !!(prop.name && prop.name.getText() === name));
114+
115+
if (!property || !isPropertyAssignment(property)) return undefined;
116+
117+
return property.initializer;
115118
};
116119

117120
export const getDecoratorName = (decorator: Decorator): string | undefined =>

test/componentChangeDetectionRule.spec.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Rule } from '../src/preferOnPushComponentChangeDetectionRule';
2+
import { assertAnnotated, assertSuccess } from './testHelper';
3+
4+
const {
5+
FAILURE_STRING,
6+
metadata: { ruleName }
7+
} = Rule;
8+
9+
describe(ruleName, () => {
10+
describe('failure', () => {
11+
it('should fail if ChangeDetectionStrategy.Default is set', () => {
12+
const source = `
13+
@Component({
14+
changeDetection: ChangeDetectionStrategy.Default
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16+
})
17+
class Test {}
18+
`;
19+
assertAnnotated({
20+
message: FAILURE_STRING,
21+
ruleName,
22+
source
23+
});
24+
});
25+
26+
it('should fail if no ChangeDetectionStrategy is explicitly set', () => {
27+
const source = `
28+
@Component({
29+
~~~~~~~~~~~~
30+
selector: 'foo'
31+
})
32+
~~
33+
class Test {}
34+
`;
35+
assertAnnotated({
36+
message: FAILURE_STRING,
37+
ruleName,
38+
source
39+
});
40+
});
41+
});
42+
43+
describe('success', () => {
44+
it('should succeed if ChangeDetectionStrategy.OnPush is set', () => {
45+
const source = `
46+
@Component({
47+
changeDetection: ChangeDetectionStrategy.OnPush
48+
})
49+
class Test {}
50+
`;
51+
assertSuccess(ruleName, source);
52+
});
53+
});
54+
});

0 commit comments

Comments
 (0)