criticizer
Version:
Linting for Angular applications, following angular.io/styleguide.
137 lines (122 loc) • 4.16 kB
text/typescript
import * as Lint from 'tslint';
import * as ts from 'typescript';
import {sprintf} from 'sprintf-js';
import {stringDistance} from './util/utils';
import {Ng2Walker} from './angular/ng2Walker';
import {RecursiveAngularExpressionVisitor} from './angular/templates/recursiveAngularExpressionVisitor';
import {ExpTypes} from './angular/expressionTypes';
import {getDeclaredMethodNames, getDeclaredPropertyNames} from './util/classDeclarationUtils';
import * as e from '@angular/compiler/src/expression_parser/ast';
import {Config} from './angular/config';
enum DeclarationType {
Property,
Method
};
export interface ASTField {
obj?: ASTField;
receiver?: ASTField;
name?: string;
span: e.ParseSpan;
}
class SymbolAccessValidator extends RecursiveAngularExpressionVisitor {
visitPropertyRead(ast: e.PropertyRead, context: any): any {
return this.doCheck(ast, DeclarationType.Property, context);
}
visitMethodCall(ast: e.MethodCall, context: any): any {
this.doCheck(ast, DeclarationType.Method, context);
}
visitPropertyWrite(ast: e.PropertyWrite, context: any): any {
this.doCheck(ast, DeclarationType.Property, context);
}
private doCheck(ast: ASTField, type: DeclarationType, context: any): any {
let symbolType: string;
let available: string[];
if (type === DeclarationType.Method) {
symbolType = 'method';
} else {
symbolType = 'property';
}
available = getDeclaredMethodNames(this.context.controller)
.concat(getDeclaredPropertyNames(this.context.controller))
.concat(this.preDefinedVariables);
// Do not support nested properties yet
let tmp = ast;
while (tmp && !ExpTypes.ImplicitReceiver(tmp)) {
ast = tmp;
if (ExpTypes.KeyedRead(tmp)) {
tmp = tmp.obj;
} else if (ExpTypes.KeyedWrite(tmp)) {
tmp = tmp.obj;
} else if (ExpTypes.PropertyRead(tmp)) {
tmp = tmp.receiver;
} else if (ExpTypes.PropertyWrite(tmp)) {
tmp = tmp.receiver;
} else if (ExpTypes.SafeMethodCall(tmp)) {
tmp = tmp.receiver;
} else if (ExpTypes.SafePropertyRead(tmp)) {
tmp = tmp.receiver;
} else if (ExpTypes.MethodCall(tmp)) {
tmp = tmp.receiver;
} else {
break;
}
}
if (available.indexOf(ast.name) < 0) {
let failureString = sprintf.apply(this, [Rule.FAILURE, symbolType, ast.name]);
if (ast.name) {
const top = this.getTopSuggestion(available, ast.name);
const getSuggestion = (list: string[]) => {
if (list.length === 1) {
return `"${list[0]}"`;
}
let result = `"${list.shift()}"`;
while (list.length > 1) {
result += `, "${list.shift()}"`;
}
result += ` or "${list.shift()}"`;
return result;
};
if (top.length && top[0].distance <= 2) {
failureString += ` Probably you mean: ${getSuggestion(top.map(s => s.element))}.`;
}
}
const width = ast.name.length;
this.addFailure(this.createFailure(ast.span.start, width, failureString));
}
return null;
}
private getTopSuggestion(list: string[], current: string) {
const result = [];
const tmp = list.map(e => {
return {
element: e,
distance: stringDistance(e, current)
};
}).sort((a, b) => a.distance - b.distance);
const first = tmp.shift();
if (!first) {
return [];
} else {
result.push(first);
let current: any;
while (current = tmp.shift()) {
if (current.distance !== first.distance) {
return result;
} else {
result.push(current);
}
}
return result;
}
}
}
export class Rule extends Lint.Rules.AbstractRule {
static FAILURE: string = 'The %s "%s" that you\'re trying to access does not exist in the class declaration.';
public apply(sourceFile:ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(
new Ng2Walker(sourceFile,
this.getOptions(), {
expressionVisitorCtrl: SymbolAccessValidator
}));
}
}