Skip to content

Commit df81b7b

Browse files
committed
Merge pull request #18 from mgechev/pipe-transform-interface
Pipe transform interface
2 parents e083812 + 465c653 commit df81b7b

5 files changed

+286
-3
lines changed

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ 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,
25+
"pipe-transform-interface":true
2426
}
2527
```
2628

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

6163
## License

src/lifeCycleHookRule.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
super.visitClassDeclaration(node);
59+
}
60+
61+
62+
private extractMissing(members:ts.NodeArray<ts.ClassElement>,
63+
syntaxKind:SyntaxKind.SyntaxKind,
64+
interfaces:Array<string>):Array<string> {
65+
let ngMembers = members.filter(m=>m.kind === syntaxKind.MethodDeclaration)
66+
.map(m=>(<any>m.name).text)
67+
.filter(n=>n.substr(0, 2) === Rule.HOOKS_PREFIX)
68+
.map(n=>n.substr(2, n.lenght))
69+
.filter(n=>Rule.LIFE_CYCLE_HOOKS_NAMES.indexOf(n) !== -1);
70+
return ngMembers.filter(m=>interfaces.indexOf(m) === -1);
71+
}
72+
73+
private formatFailure(className:string, missing:Array<string>):Array<string> {
74+
let failureConfig:Array<string>;
75+
if (missing.length === 1) {
76+
failureConfig = [Rule.FAILURE_SINGLE, className, Rule.HOOKS_PREFIX + missing[0], missing[0]];
77+
} else {
78+
let joinedNgMissing:string = missing.map(m=>Rule.HOOKS_PREFIX + m).join(', ');
79+
let joinedMissingInterfaces = missing.join(', ');
80+
failureConfig = [Rule.FAILURE_MANY, className, joinedNgMissing, joinedMissingInterfaces];
81+
}
82+
return failureConfig;
83+
}
84+
}

src/pipeTransformInterfaceRule.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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:string = 'The %s class has the Pipe decorator, so it should implement the PipeTransform interface';
15+
16+
static PIPE_INTERFACE_NAME = 'PipeTransform';
17+
}
18+
19+
export class ClassMetadataWalker extends Lint.RuleWalker {
20+
21+
visitClassDeclaration(node:ts.ClassDeclaration) {
22+
let decorators = node.decorators;
23+
if (decorators) {
24+
let pipes:Array<string> = decorators.map(d=>(<any>d.expression).expression.text).filter(t=>t === 'Pipe');
25+
if (pipes.length !== 0) {
26+
let className:string = node.name.text;
27+
if (!this.hasIPipeTransform(node)) {
28+
this.addFailure(
29+
this.createFailure(
30+
node.getStart(),
31+
node.getWidth(),
32+
sprintf.apply(this, [Rule.FAILURE, className])));
33+
}
34+
}
35+
}
36+
super.visitClassDeclaration(node);
37+
}
38+
39+
private hasIPipeTransform(node:ts.ClassDeclaration):boolean {
40+
let interfaces = [];
41+
if (node.heritageClauses) {
42+
let interfacesClause = node.heritageClauses
43+
.filter(h=>h.token === SyntaxKind.current().ImplementsKeyword);
44+
if (interfacesClause.length !== 0) {
45+
interfaces = interfacesClause[0].types
46+
.map(t=>(<any>t.expression).text);
47+
}
48+
}
49+
return interfaces.indexOf(Rule.PIPE_INTERFACE_NAME) !== -1;
50+
}
51+
}

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+
});
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {assertFailure, assertSuccess} from './testHelper';
2+
3+
describe('pipe-transform-interface', () => {
4+
describe('invalid declaration of pipe', () => {
5+
it(`should fail, when a Pipe is declared without implementing the PipeTransform interface`, () => {
6+
let source = `
7+
@Pipe({name: 'fetch'})
8+
export class NewPipe{
9+
transform(url:string):any {
10+
}
11+
}`;
12+
assertFailure('pipe-transform-interface', source, {
13+
message: 'The NewPipe class has the Pipe decorator, so it should implement the PipeTransform interface',
14+
startPosition: {
15+
line: 1,
16+
character: 24
17+
},
18+
endPosition: {
19+
line: 5,
20+
character: 25
21+
}
22+
});
23+
});
24+
});
25+
describe('valid use of Pipe with the implementation of the PipeTransform interface', () => {
26+
it(`should succeed when Pipe is declared properly`, () => {
27+
let source = `
28+
@Pipe({name: 'fetch'})
29+
export class NewPipe implements PipeTransform{
30+
transform(url:string):any {
31+
}
32+
}`;
33+
assertSuccess('pipe-transform-interface', source);
34+
});
35+
});
36+
describe('valid use of empty class', () => {
37+
it(`should succeed, when Pipe is not used`, () => {
38+
let source = `class App{}`;
39+
assertSuccess('pipe-transform-interface', source);
40+
});
41+
});
42+
});

0 commit comments

Comments
 (0)