Skip to content

Commit d074771

Browse files
author
PreskoIsTheGreatest
committed
add life cycle hooks rule and updated README
1 parent e083812 commit d074771

File tree

3 files changed

+190
-2
lines changed

3 files changed

+190
-2
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ Below you can find a recommended configuration which is based on the [Angular 2
2020
"attribute-parameter-decorator": true,
2121
"input-property-directive": true,
2222
"output-property-directive": true,
23-
"call-forward-ref":true
23+
"call-forward-ref":true,
24+
"life-cycle-hook":true
2425
}
2526
```
2627

@@ -54,7 +55,7 @@ Below you can find a recommended configuration which is based on the [Angular 2
5455
- [ ] Locate tests in the same directory (rise optional warning when no test file is found).
5556
- [ ] Rise warning on complex logic inside of the templates.
5657
- [ ] Do not manipulate elements referenced within the template.
57-
- [ ] Implement life-cycle hooks explicitly.
58+
- [x] Implement life-cycle hooks explicitly.
5859
- [ ] Implement Pipe transform interface for pipes.
5960
- [ ] Proper naming for pipes (kebab-case, optionally prefixed).
6061

src/lifeCycleHookRule.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as Lint from 'tslint/lib/lint';
2+
import * as ts from 'typescript';
3+
import {sprintf} from 'sprintf-js';
4+
import SyntaxKind = require('./util/syntaxKind');
5+
6+
export class Rule extends Lint.Rules.AbstractRule {
7+
8+
public apply(sourceFile:ts.SourceFile):Lint.RuleFailure[] {
9+
return this.applyWithWalker(
10+
new ClassMetadataWalker(sourceFile,
11+
this.getOptions()));
12+
}
13+
14+
static FAILURE_SINGLE:string = 'In class %s the method %s is a life cycle hook' +
15+
' and should implement the %s interface';
16+
17+
static FAILURE_MANY = 'In class %s the methods - %s' +
18+
' are life cycle hooks and should implement the interfaces: %s';
19+
20+
static HOOKS_PREFIX = 'ng';
21+
22+
static LIFE_CYCLE_HOOKS_NAMES:Array<any> = [
23+
"OnChanges",
24+
"OnInit",
25+
"DoCheck",
26+
"AfterContentInit",
27+
"AfterContentChecked",
28+
"AfterViewInit",
29+
"AfterViewChecked",
30+
"OnDestroy"
31+
]
32+
33+
}
34+
35+
export class ClassMetadataWalker extends Lint.RuleWalker {
36+
37+
visitClassDeclaration(node:ts.ClassDeclaration) {
38+
let syntaxKind = SyntaxKind.current();
39+
let className = node.name.text;
40+
41+
let interfaces = [];
42+
if (node.heritageClauses) {
43+
let interfacesClause = node.heritageClauses.filter(h=>h.token === syntaxKind.ImplementsKeyword);
44+
if (interfacesClause.length !== 0) {
45+
interfaces = interfacesClause[0].types.map(t=>(<any>t.expression).text);
46+
}
47+
}
48+
49+
let missing:Array<string> = this.extractMissing(node.members, syntaxKind, interfaces);
50+
51+
if (missing.length !== 0) {
52+
this.addFailure(
53+
this.createFailure(
54+
node.getStart(),
55+
node.getWidth(),
56+
sprintf.apply(this, this.formatFailure(className, missing))));
57+
}
58+
}
59+
60+
61+
private extractMissing(members:ts.NodeArray<ts.ClassElement>,
62+
syntaxKind:SyntaxKind.SyntaxKind,
63+
interfaces:Array<string>):Array<string> {
64+
let ngMembers = members.filter(m=>m.kind === syntaxKind.MethodDeclaration)
65+
.map(m=>(<any>m.name).text)
66+
.filter(n=>n.substr(0, 2) === Rule.HOOKS_PREFIX)
67+
.map(n=>n.substr(2, n.lenght))
68+
.filter(n=>Rule.LIFE_CYCLE_HOOKS_NAMES.indexOf(n) !== -1);
69+
return ngMembers.filter(m=>interfaces.indexOf(m) === -1);
70+
}
71+
72+
private formatFailure(className:string, missing:Array<string>):Array<string> {
73+
let failureConfig:Array<string>;
74+
if (missing.length === 1) {
75+
failureConfig = [Rule.FAILURE_SINGLE, className, Rule.HOOKS_PREFIX + missing[0], missing[0]];
76+
} else {
77+
let joinedNgMissing:string = missing.map(m=>Rule.HOOKS_PREFIX + m).join(', ');
78+
let joinedMissingInterfaces = missing.join(', ');
79+
failureConfig = [Rule.FAILURE_MANY, className, joinedNgMissing, joinedMissingInterfaces];
80+
}
81+
return failureConfig;
82+
}
83+
}

test/lifeCycleHookRule.spec.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {assertFailure, assertSuccess} from './testHelper';
2+
3+
describe('life-cycle-hook', () => {
4+
describe('invalid declaration of life hook', () => {
5+
it(`should fail, when a life cycle hook is used without implementing it's interface`, () => {
6+
let source = `
7+
class App {
8+
ngOnInit(){
9+
}
10+
}`;
11+
assertFailure('life-cycle-hook', source, {
12+
message: 'In class App the method ngOnInit is a life cycle hook and should implement the OnInit interface',
13+
startPosition: {
14+
line: 1,
15+
character: 12
16+
},
17+
endPosition: {
18+
line: 4,
19+
character: 13
20+
}
21+
});
22+
});
23+
});
24+
describe('invalid declaration of life hooks', () => {
25+
it(`should fail, when life cycle hooks are used without implementing their interfaces`, () => {
26+
let source = `
27+
class App {
28+
ngOnInit(){
29+
}
30+
ngOnDestroy(){
31+
}
32+
}`;
33+
assertFailure('life-cycle-hook', source, {
34+
message: 'In class App the methods - ngOnInit, ngOnDestroy are life cycle hooks and ' +
35+
'should implement the interfaces: OnInit, OnDestroy',
36+
startPosition: {
37+
line: 1,
38+
character: 12
39+
},
40+
endPosition: {
41+
line: 6,
42+
character: 13
43+
}
44+
});
45+
});
46+
});
47+
describe('invalid declaration of life hooks', () => {
48+
it(`should fail, when some of the life cycle hooks are used without implementing their interfaces`, () => {
49+
let source = `
50+
class App extends Component implements OnInit{
51+
ngOnInit(){
52+
}
53+
ngOnDestroy(){
54+
}
55+
}`;
56+
assertFailure('life-cycle-hook', source, {
57+
message: 'In class App the method ngOnDestroy is a life cycle hook and should implement the OnDestroy interface',
58+
startPosition: {
59+
line: 1,
60+
character: 12
61+
},
62+
endPosition: {
63+
line: 6,
64+
character: 13
65+
}
66+
});
67+
});
68+
});
69+
describe('valid declaration of life hook', () => {
70+
it(`should succeed, when life cycle hook is used with it's corresponding interface`, () => {
71+
let source = `
72+
class App implements OnInit {
73+
ngOnInit(){
74+
}
75+
}`;
76+
assertSuccess('life-cycle-hook', source);
77+
});
78+
});
79+
describe('valid declaration of life hooks', () => {
80+
it(`should succeed, when life cycle hooks are used with their corresponding interfaces`, () => {
81+
let source = `
82+
class App extends Component implements OnInit,OnDestroy {
83+
ngOnInit(){
84+
}
85+
86+
private ngOnChanges:string="";
87+
88+
ngOnDestroy(){
89+
}
90+
91+
ngOnSmth{
92+
}
93+
}`;
94+
assertSuccess('life-cycle-hook', source);
95+
});
96+
});
97+
describe('valid use of class without interfaces and life cycle hooks', () => {
98+
it(`should succeed when life cycle hooks are not used`, () => {
99+
let source = `
100+
class App{}`;
101+
assertSuccess('life-cycle-hook', source);
102+
});
103+
});
104+
});

0 commit comments

Comments
 (0)