Skip to content

Commit e521115

Browse files
rafaelss95mgechev
authored andcommitted
feat(rule): add no-conflicting-life-cycle-hooks rule (#563)
1 parent 3d652d1 commit e521115

File tree

3 files changed

+175
-0
lines changed

3 files changed

+175
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { Rule as I18nRule } from './i18nRule';
1111
export { Rule as ImportDestructuringSpacingRule } from './importDestructuringSpacingRule';
1212
export { Rule as MaxInlineDeclarationsRule } from './maxInlineDeclarationsRule';
1313
export { Rule as NoAttributeParameterDecoratorRule } from './noAttributeParameterDecoratorRule';
14+
export { Rule as NoConflictingLifeCycleHooksRule } from './noConflictingLifeCycleHooksRule';
1415
export { Rule as NoForwardRefRule } from './noForwardRefRule';
1516
export { Rule as NoInputPrefixRule } from './noInputPrefixRule';
1617
export { Rule as NoInputRenameRule } from './noInputRenameRule';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { sprintf } from 'sprintf-js';
2+
import * as Lint from 'tslint';
3+
import * as ts from 'typescript';
4+
5+
export class Rule extends Lint.Rules.AbstractRule {
6+
public static metadata: Lint.IRuleMetadata = {
7+
ruleName: 'no-conflicting-life-cycle-hooks',
8+
type: 'maintainability',
9+
description: 'Ensure that directives not implement conflicting lifecycle hooks.',
10+
descriptionDetails: 'See more at https://angular.io/api/core/DoCheck#description.',
11+
rationale: 'A directive typically should not use both DoCheck and OnChanges to respond ' +
12+
'to changes on the same input, as ngOnChanges will continue to be called when the ' +
13+
'default change detector detects changes.',
14+
options: null,
15+
optionsDescription: 'Not configurable.',
16+
typescriptOnly: true,
17+
};
18+
19+
static FAILURE_STRING = 'Implement DoCheck and OnChanges hooks in class %s is not recommended';
20+
21+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
22+
return this.applyWithWalker(new ClassMetadataWalker(sourceFile, this.getOptions()));
23+
}
24+
}
25+
26+
const hooksPrefix = 'ng';
27+
const lifecycleHooksMethods: string[] = [
28+
'DoCheck',
29+
'OnChanges'
30+
];
31+
32+
export class ClassMetadataWalker extends Lint.RuleWalker {
33+
visitClassDeclaration(node: ts.ClassDeclaration) {
34+
this.validateInterfaces(node);
35+
this.validateMethods(node);
36+
super.visitClassDeclaration(node);
37+
}
38+
39+
private validateInterfaces(node: ts.ClassDeclaration): void {
40+
if (!node.heritageClauses) {
41+
return;
42+
}
43+
44+
const interfacesClause = node.heritageClauses.find(h => h.token === ts.SyntaxKind.ImplementsKeyword);
45+
46+
if (!interfacesClause) {
47+
return;
48+
}
49+
50+
const interfaces = interfacesClause.types.map<string>((t: any) => {
51+
return t.expression.name ? t.expression.name.text : t.expression.text;
52+
});
53+
const matchesAllHooks = lifecycleHooksMethods.every(l => interfaces.indexOf(l) !== -1);
54+
55+
if (matchesAllHooks) {
56+
this.addFailureAtNode(node, sprintf.apply(this, [Rule.FAILURE_STRING, node.name.text]));
57+
}
58+
}
59+
60+
private validateMethods(node: ts.ClassDeclaration): void {
61+
const methodNames = node.members
62+
.filter(m => m.kind === ts.SyntaxKind.MethodDeclaration)
63+
.map<string>(m => (m.name as any).text);
64+
const matchesAllHooks = lifecycleHooksMethods.every(l => {
65+
return methodNames.indexOf(`${hooksPrefix}${l}`) !== -1;
66+
});
67+
68+
if (matchesAllHooks) {
69+
this.addFailureAtNode(node, sprintf.apply(this, [Rule.FAILURE_STRING, node.name.text]));
70+
}
71+
}
72+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { assertFailures, assertSuccess, IExpectedFailure } from './testHelper';
2+
3+
const ruleName = 'no-conflicting-life-cycle-hooks';
4+
const fails: IExpectedFailure[] = [
5+
{
6+
endPosition: {
7+
line: 4,
8+
character: 9
9+
},
10+
message: 'Implement DoCheck and OnChanges hooks in class Test is not recommended',
11+
startPosition: {
12+
line: 1,
13+
character: 8
14+
}
15+
}
16+
];
17+
18+
describe(ruleName, () => {
19+
describe('failure', () => {
20+
it('should fail when implements both DoCheck and OnChanges hooks', () => {
21+
const source = `
22+
class Test implements DoCheck, OnChanges {
23+
test() {}
24+
test1() {}
25+
}
26+
`;
27+
assertFailures(ruleName, source, fails);
28+
});
29+
30+
it('should fail when implements both DoCheck and OnChanges hooks/methods', () => {
31+
const source = `
32+
class Test implements DoCheck, OnChanges {
33+
ngDoCheck() {}
34+
ngOnChanges() {}
35+
}
36+
`;
37+
assertFailures(ruleName, source, fails.concat(fails));
38+
});
39+
40+
it('should fail have both ngDoCheck and ngOnChanges methods exist', () => {
41+
const source = `
42+
class Test {
43+
ngDoCheck() {}
44+
ngOnChanges() {}
45+
}
46+
`;
47+
assertFailures(ruleName, source, fails);
48+
});
49+
});
50+
51+
describe('success', () => {
52+
it('should pass when contain ngDoCheck, but not ngOnChanges method', () => {
53+
const source = `
54+
class Test {
55+
ngDoCheck() {}
56+
}
57+
`;
58+
assertSuccess(ruleName, source);
59+
});
60+
61+
it('should pass when implements DoCheck, but not OnChanges hook', () => {
62+
const source = `
63+
class Test implements DoCheck {}
64+
`;
65+
assertSuccess(ruleName, source);
66+
});
67+
68+
it('should pass when implementing and contain DoCheck hook/method, but not OnChanges hook/method', () => {
69+
const source = `
70+
class Test implements DoCheck {
71+
ngDoCheck() {}
72+
}
73+
`;
74+
assertSuccess(ruleName, source);
75+
});
76+
77+
it('should pass when contain ngOnChanges, but not ngDoCheck method', () => {
78+
const source = `
79+
class Test {
80+
ngOnChanges() {}
81+
}
82+
`;
83+
assertSuccess(ruleName, source);
84+
});
85+
86+
it('should pass when implements OnChanges, but not DoCheck hook', () => {
87+
const source = `
88+
class Test implements OnChanges {}
89+
`;
90+
assertSuccess(ruleName, source);
91+
});
92+
93+
it('should pass when implementing and contain OnChanges hook/method, but not DoCheck hook/method', () => {
94+
const source = `
95+
class Test implements OnChanges {
96+
ngOnChanges() {}
97+
}
98+
`;
99+
assertSuccess(ruleName, source);
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)