|
| 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 | +} |
0 commit comments