diff --git a/src/angular/ng2Walker.ts b/src/angular/ng2Walker.ts index c90294ad6..daca97b9c 100644 --- a/src/angular/ng2Walker.ts +++ b/src/angular/ng2Walker.ts @@ -32,6 +32,7 @@ export interface Ng2WalkerConfig { expressionVisitorCtrl?: RecursiveAngularExpressionVisitorCtr; templateVisitorCtrl?: TemplateAstVisitorCtr; cssVisitorCtrl?: CssAstVisitorCtrl; + languageService?: ts.LanguageService; } export class Ng2Walker extends Lint.RuleWalker { @@ -41,11 +42,6 @@ export class Ng2Walker extends Lint.RuleWalker { protected _metadataReader?: MetadataReader) { super(sourceFile, _originalOptions); this._metadataReader = this._metadataReader || ng2WalkerFactoryUtils.defaultMetadataReader(); - this._config = Object.assign({ - templateVisitorCtrl: BasicTemplateAstVisitor, - expressionVisitorCtrl: RecursiveAngularExpressionVisitor, - cssVisitorCtrl: BasicCssAstVisitor - }, this._config || {}); this._config = Object.assign({ templateVisitorCtrl: BasicTemplateAstVisitor, @@ -169,7 +165,8 @@ export class Ng2Walker extends Lint.RuleWalker { const referenceVisitor = new ReferenceCollectorVisitor(); const visitor = new this._config.templateVisitorCtrl( - sourceFile, this._originalOptions, context, baseStart, this._config.expressionVisitorCtrl); + sourceFile, this._originalOptions, context, baseStart, this._config.expressionVisitorCtrl, + this._config.languageService); compiler.templateVisitAll(referenceVisitor, roots, null); visitor._variables = referenceVisitor.variables; compiler.templateVisitAll(visitor, roots, context.controller); diff --git a/src/angular/templates/basicTemplateAstVisitor.ts b/src/angular/templates/basicTemplateAstVisitor.ts index d5e475d7f..12e79a90c 100644 --- a/src/angular/templates/basicTemplateAstVisitor.ts +++ b/src/angular/templates/basicTemplateAstVisitor.ts @@ -64,13 +64,14 @@ const getExpressionDisplacement = (binding: any) => { export interface RecursiveAngularExpressionVisitorCtr { - new(sourceFile: ts.SourceFile, options: Lint.IOptions, context: ComponentMetadata, basePosition: number); + new(sourceFile: ts.SourceFile, options: Lint.IOptions, context: ComponentMetadata, basePosition: number, + languageService?: ts.LanguageService); } export interface TemplateAstVisitorCtr { new(sourceFile: ts.SourceFile, options: Lint.IOptions, context: ComponentMetadata, - templateStart: number, expressionVisitorCtrl: RecursiveAngularExpressionVisitorCtr); + templateStart: number, expressionVisitorCtrl: RecursiveAngularExpressionVisitorCtr, languageService?: ts.LanguageService); } export class BasicTemplateAstVisitor extends SourceMappingVisitor implements ast.TemplateAstVisitor { @@ -80,13 +81,14 @@ export class BasicTemplateAstVisitor extends SourceMappingVisitor implements ast private _originalOptions: Lint.IOptions, protected context: ComponentMetadata, protected templateStart: number, - private expressionVisitorCtrl: RecursiveAngularExpressionVisitorCtr = RecursiveAngularExpressionVisitor) { + private expressionVisitorCtrl: RecursiveAngularExpressionVisitorCtr = RecursiveAngularExpressionVisitor, + protected languageService?: ts.LanguageService) { super(sourceFile, _originalOptions, context.template.template, templateStart); } protected visitNg2TemplateAST(ast: e.AST, templateStart: number) { const templateVisitor = - new this.expressionVisitorCtrl(this.getSourceFile(), this._originalOptions, this.context, templateStart); + new this.expressionVisitorCtrl(this.getSourceFile(), this._originalOptions, this.context, templateStart, this.languageService); templateVisitor.preDefinedVariables = this._variables; templateVisitor.visit(ast); templateVisitor.getFailures().forEach(f => this.addFailure(f)); diff --git a/src/angular/templates/recursiveAngularExpressionVisitor.ts b/src/angular/templates/recursiveAngularExpressionVisitor.ts index 702eea9de..818153eb6 100644 --- a/src/angular/templates/recursiveAngularExpressionVisitor.ts +++ b/src/angular/templates/recursiveAngularExpressionVisitor.ts @@ -9,7 +9,7 @@ export class RecursiveAngularExpressionVisitor extends SourceMappingVisitor impl public preDefinedVariables = []; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, - protected context: ComponentMetadata, protected basePosition: number) { + protected context: ComponentMetadata, protected basePosition: number, protected languageService: ts.LanguageService) { super(sourceFile, options, context.template.template, basePosition); } diff --git a/src/noAccessMissingMemberRule.ts b/src/noAccessMissingMemberRule.ts index 8e0a6d0d4..df6796b3f 100644 --- a/src/noAccessMissingMemberRule.ts +++ b/src/noAccessMissingMemberRule.ts @@ -5,7 +5,7 @@ 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 {getClassMembers} from './util/classDeclarationUtils'; import * as e from '@angular/compiler/src/expression_parser/ast'; import {Config} from './angular/config'; @@ -44,8 +44,10 @@ class SymbolAccessValidator extends RecursiveAngularExpressionVisitor { symbolType = 'property'; } - available = getDeclaredMethodNames(this.context.controller) - .concat(getDeclaredPropertyNames(this.context.controller)) + const typeChecker = this.languageService.getProgram().getTypeChecker(); + + available = getClassMembers(this.context.controller, typeChecker) + .map(p => p.name) .concat(this.preDefinedVariables); // Do not support nested properties yet @@ -122,14 +124,24 @@ class SymbolAccessValidator extends RecursiveAngularExpressionVisitor { } } -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.'; +export class Rule extends Lint.Rules.TypedRule { + public static FAILURE: string = 'The %s "%s" that you\'re trying to access does not exist in the class declaration.'; + public static metadata: Lint.IRuleMetadata = { + ruleName: 'no-access-missing-member', + description: 'Prevents bindings to expressions containing non-existing methods or properties', + optionsDescription: 'Not configurable', + options: null, + type: 'functionality', + typescriptOnly: true + }; - public apply(sourceFile:ts.SourceFile): Lint.RuleFailure[] { + public applyWithProgram(sourceFile: ts.SourceFile, languageService: ts.LanguageService): Lint.RuleFailure[] { + const sf = languageService.getProgram().getSourceFiles().filter(sf => sf.fileName === sourceFile.fileName).pop(); return this.applyWithWalker( - new Ng2Walker(sourceFile, + new Ng2Walker(sf, this.getOptions(), { - expressionVisitorCtrl: SymbolAccessValidator + expressionVisitorCtrl: SymbolAccessValidator, + languageService })); } } diff --git a/src/util/classDeclarationUtils.ts b/src/util/classDeclarationUtils.ts index 0e690878f..66ca5af3c 100644 --- a/src/util/classDeclarationUtils.ts +++ b/src/util/classDeclarationUtils.ts @@ -3,6 +3,11 @@ import { current } from './syntaxKind'; const SyntaxKind = current(); +export const getClassMembers = (declaration: ts.ClassDeclaration, tc: ts.TypeChecker): ts.Symbol[] => { + const properties = tc.getTypeAtLocation(declaration).getProperties(); + return properties; +}; + export const getDeclaredProperties = (declaration: ts.ClassDeclaration) => { const m = declaration.members; const ctr = m.filter((m: any) => m.kind === SyntaxKind.Constructor).pop(); diff --git a/test/noAccessMissingMemberRule.spec.ts b/test/noAccessMissingMemberRule.spec.ts index 53b958fc8..cbced578b 100644 --- a/test/noAccessMissingMemberRule.spec.ts +++ b/test/noAccessMissingMemberRule.spec.ts @@ -1,25 +1,26 @@ -import {assertFailure, assertSuccess} from './testHelper'; +import { assertFailure, assertSuccess } from './testHelper'; import {Config} from '../src/angular/config'; describe('no-access-missing-member', () => { describe('invalid expressions', () => { it('should fail when interpolating missing property', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ foo }}
+ template: '
{{ foo }}
' }) - class Test { + export class Test { bar: number; }`; assertFailure('no-access-missing-member', source, { message: 'The property "foo" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 3, + line: 4, character: 29 }, endPosition: { - line: 3, + line: 4, character: 32 } }); @@ -27,19 +28,20 @@ describe('no-access-missing-member', () => { it('should work with existing properties and pipes', () => { let source = ` + @Component({ selector: 'foobar', template: \`
\` }) - class Test {}`; + export class Test {}`; assertFailure('no-access-missing-member', source, { message: 'The property "showMenu" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 3, + line: 4, character: 40 }, endPosition: { - line: 3, + line: 4, character: 48 } }); @@ -47,21 +49,22 @@ describe('no-access-missing-member', () => { it('should fail when using missing method', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ baz() }}
+ template: '
{{ baz() }}
' }) - class Test { + export class Test { bar() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "baz" that you\'re trying to access does not exist in the class declaration. Probably you mean: "bar".', startPosition: { - line: 3, + line: 4, character: 29 }, endPosition: { - line: 3, + line: 4, character: 32 } }); @@ -69,21 +72,22 @@ describe('no-access-missing-member', () => { it('should fail when using missing method in an interpolation mixed with text', () => { let source = ` + @Component({ selector: 'foobar', - template: '
test {{ baz() }}
+ template: '
test {{ baz() }}
' }) - class Test { + export class Test { bar() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "baz" that you\'re trying to access does not exist in the class declaration. Probably you mean: "bar".', startPosition: { - line: 3, + line: 4, character: 35 }, endPosition: { - line: 3, + line: 4, character: 38 } }); @@ -91,21 +95,22 @@ describe('no-access-missing-member', () => { it('should fail when using missing method in an interpolation mixed with text and interpolation', () => { let source = ` + @Component({ selector: 'foobar', - template: '
test {{ bar() }} {{ baz() }}
+ template: '
test {{ bar() }} {{ baz() }}
' }) - class Test { + export class Test { bar() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "baz" that you\'re trying to access does not exist in the class declaration. Probably you mean: "bar".', startPosition: { - line: 3, + line: 4, character: 47 }, endPosition: { - line: 3, + line: 4, character: 50 } }); @@ -113,21 +118,22 @@ describe('no-access-missing-member', () => { it('should fail when using missing method in an interpolation mixed with text, interpolation & binary expression', () => { let source = ` + @Component({ selector: 'foobar', - template: '
test {{ bar() }} {{ bar() + baz() }}
+ template: '
test {{ bar() }} {{ bar() + baz() }}
' }) - class Test { + export class Test { bar() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "baz" that you\'re trying to access does not exist in the class declaration. Probably you mean: "bar".', startPosition: { - line: 3, + line: 4, character: 55 }, endPosition: { - line: 3, + line: 4, character: 58 } }); @@ -135,21 +141,22 @@ describe('no-access-missing-member', () => { it('should fail in binary operation with missing property', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ baz2() + foo }}
+ template: '
{{ baz2() + foo }}
' }) - class Test { + export class Test { baz2() {} }`; assertFailure('no-access-missing-member', source, { message: 'The property "foo" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 3, + line: 4, character: 38 }, endPosition: { - line: 3, + line: 4, character: 41 } }); @@ -157,22 +164,23 @@ describe('no-access-missing-member', () => { it('should fail fail in binary operation with missing method', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ baz() + getPrsonName(1, 2, 3) }}
+ template: '
{{ baz() + getPrsonName(1, 2, 3) }}
' }) - class Test { + export class Test { baz() {} getPersonName() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "getPrsonName" that you\'re trying to access does not exist in the class declaration. Probably you mean: "getPersonName".', startPosition: { - line: 3, + line: 4, character: 37 }, endPosition: { - line: 3, + line: 4, character: 49 } }); @@ -180,21 +188,22 @@ describe('no-access-missing-member', () => { it('should fail with property binding and missing method', () => { let source = ` + @Component({ selector: 'foobar', - template: '
+ template: '
' }) - class Test { + export class Test { baz() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "bar" that you\'re trying to access does not exist in the class declaration. Probably you mean: "baz".', startPosition: { - line: 3, + line: 4, character: 39 }, endPosition: { - line: 3, + line: 4, character: 42 } }); @@ -202,21 +211,22 @@ describe('no-access-missing-member', () => { it('should fail with style binding and missing method', () => { let source = ` + @Component({ selector: 'foobar', - template: '
+ template: '
' }) - class Test { + export class Test { baz() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "bar" that you\'re trying to access does not exist in the class declaration. Probably you mean: "baz".', startPosition: { - line: 3, + line: 4, character: 41 }, endPosition: { - line: 3, + line: 4, character: 44 } }); @@ -224,21 +234,22 @@ describe('no-access-missing-member', () => { it('should fail on event handling with missing method', () => { let source = ` + @Component({ selector: 'foobar', - template: '
+ template: '
' }) - class Test { + export class Test { baz() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "bar" that you\'re trying to access does not exist in the class declaration. Probably you mean: "baz".', startPosition: { - line: 3, + line: 4, character: 35 }, endPosition: { - line: 3, + line: 4, character: 38 } }); @@ -246,21 +257,22 @@ describe('no-access-missing-member', () => { it('should fail on event handling on the right position with a lot of whitespace', () => { let source = ` + @Component({ selector: 'foobar', - template: '
+ template: '
' }) - class Test { + export class Test { baz() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "bar" that you\'re trying to access does not exist in the class declaration. Probably you mean: "baz".', startPosition: { - line: 3, + line: 4, character: 41 }, endPosition: { - line: 3, + line: 4, character: 44 } }); @@ -268,22 +280,23 @@ describe('no-access-missing-member', () => { it('should fail on event handling on the right position with spaces and newlines', () => { let source = ` + @Component({ selector: 'foobar', template: \`
\` }) - class Test { + export class Test { baz() {} }`; assertFailure('no-access-missing-member', source, { message: 'The method "bar" that you\'re trying to access does not exist in the class declaration. Probably you mean: "baz".', startPosition: { - line: 4, + line: 5, character: 12 }, endPosition: { - line: 4, + line: 5, character: 15 } }); @@ -292,11 +305,12 @@ describe('no-access-missing-member', () => { it('should not throw when template ref used outside component scope', () => { let source = ` + @Component({ selector: 'foobar', template: '
' }) - class Test { + export class Test { foo: number; }`; assertSuccess('no-access-missing-member', source); @@ -304,22 +318,24 @@ describe('no-access-missing-member', () => { it('should not throw when routerLinkActive template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '{{ test }}' }) - class Test {}`; + export class Test {}`; assertSuccess('no-access-missing-member', source); }); it('should not throw when ngModel template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '{{ test }}' }) - class Test { + export class Test { foo: string; }`; assertSuccess('no-access-missing-member', source); @@ -327,11 +343,12 @@ describe('no-access-missing-member', () => { it('should not throw when [md-menu-item] template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '
{{ test }}
' }) - class Test { + export class Test { foo: string; }`; assertSuccess('no-access-missing-member', source); @@ -339,11 +356,12 @@ describe('no-access-missing-member', () => { it('should not throw when md-menu template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '{{ test }}' }) - class Test { + export class Test { foo: string; }`; assertSuccess('no-access-missing-member', source); @@ -351,61 +369,66 @@ describe('no-access-missing-member', () => { it('should not throw when md-button-toggle-group template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '{{ test }}' }) - class Test {}`; + export class Test {}`; assertSuccess('no-access-missing-member', source); }); it('should not throw when md-menu-trigger-for template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '
{{ test }}
' }) - class Test {}`; + export class Test {}`; assertSuccess('no-access-missing-member', source); }); it('should not throw when mdTooltip template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '
{{ test }}
' }) - class Test {}`; + export class Test {}`; assertSuccess('no-access-missing-member', source); }); it('should not throw when mdSelect template ref is used in component', () => { let source = ` + @Component({ selector: 'foobar', template: '{{ test }}' }) - class Test {}`; + export class Test {}`; assertSuccess('no-access-missing-member', source); }); it('should fail with missing ref', () => { let source = ` + @Component({ selector: 'foobar', template: '' }) - class Test { + export class Test { foo: number; }`; assertFailure('no-access-missing-member', source, { message: 'The property "todoForm" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 3, + line: 4, character: 63 }, endPosition: { - line: 3, + line: 4, character: 71 } }); @@ -413,11 +436,12 @@ describe('no-access-missing-member', () => { it('should succeed with elementref', () => { let source = ` + @Component({ selector: 'foobar', template: '{{ baz.value }}' }) - class Test { + export class Test { foo: number; }`; assertSuccess('no-access-missing-member', source); @@ -428,11 +452,12 @@ describe('no-access-missing-member', () => { describe('valid expressions', () => { it('should succeed with "ngForm" ref', () => { let source = ` + @Component({ selector: 'foobar', template: '
' }) - class Test { + export class Test { foo: number; }`; assertSuccess('no-access-missing-member', source); @@ -440,11 +465,12 @@ describe('no-access-missing-member', () => { it('should support custom template refs', () => { let source = ` + @Component({ selector: 'foobar', template: '' }) - class Test { + export class Test { foo: number; }`; Config.predefinedDirectives.push({ @@ -457,11 +483,12 @@ describe('no-access-missing-member', () => { it('should succeed with inline property declaration', () => { let source = ` + @Component({ selector: 'foobar', template: '{{ foo }}' }) - class Test { + export class Test { constructor(public foo: number) {} }`; assertSuccess('no-access-missing-member', source); @@ -469,11 +496,12 @@ describe('no-access-missing-member', () => { it('should succeed with declared property', () => { let source = ` + @Component({ selector: 'foobar', template: '
{{ foo }}
' }) - class Test { + export class Test { foo: number; }`; assertSuccess('no-access-missing-member', source); @@ -481,11 +509,12 @@ describe('no-access-missing-member', () => { it('should succeed on declared method', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ foo() }}
+ template: '
{{ foo() }}
' }) - class Test { + export class Test { foo() {} }`; assertSuccess('no-access-missing-member', source); @@ -495,11 +524,12 @@ describe('no-access-missing-member', () => { describe('nested properties and pipes', () => { it('should work with existing single-level nested properties', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ foo.bar }}
+ template: '
{{ foo.bar }}
' }) - class Test { + export class Test { foo = {}; }`; assertSuccess('no-access-missing-member', source); @@ -507,21 +537,22 @@ describe('no-access-missing-member', () => { it('should work with existing single-level non-existing nested properties', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ foo.bar }}
+ template: '
{{ foo.bar }}
' }) - class Test { + export class Test { foo1 = {}; }`; assertFailure('no-access-missing-member', source, { message: 'The property "foo" that you\'re trying to access does not exist in the class declaration. Probably you mean: "foo1".', startPosition: { - line: 3, + line: 4, character: 29 }, endPosition: { - line: 3, + line: 4, character: 32 } }); @@ -529,11 +560,12 @@ describe('no-access-missing-member', () => { it('should work with existing properties and pipes', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ foo | baz }}
+ template: '
{{ foo | baz }}
' }) - class Test { + export class Test { foo = {}; }`; assertSuccess('no-access-missing-member', source); @@ -541,11 +573,12 @@ describe('no-access-missing-member', () => { it('should work with existing properties and pipes', () => { let source = ` + @Component({ selector: 'foobar', - template: '
{{ foo.baz() }}
+ template: '
{{ foo.baz() }}
' }) - class Test { + export class Test { foo = {}; }`; assertSuccess('no-access-missing-member', source); @@ -553,11 +586,12 @@ describe('no-access-missing-member', () => { it('should work with existing properties and pipes', () => { let source = ` + @Component({ selector: 'foobar', template: \`
\` }) - class Test { + export class Test { showMenu = {}; }`; assertSuccess('no-access-missing-member', source); @@ -565,6 +599,7 @@ describe('no-access-missing-member', () => { it('should work with inputs with string values', () => { let source = ` + @Component({ selector: 'foobar', template: \` @@ -574,7 +609,7 @@ describe('no-access-missing-member', () => { \` }) - class Test { + export class Test { public hasOrdered: boolean; }`; assertSuccess('no-access-missing-member', source); @@ -582,11 +617,12 @@ describe('no-access-missing-member', () => { it('should work with getters', () => { let source = ` + @Component({ selector: 'foobar', template: \`{{ bar }}\` }) - class Test { + export class Test { get bar() { return 42; } @@ -596,11 +632,12 @@ describe('no-access-missing-member', () => { it('should work with setters', () => { let source = ` + @Component({ selector: 'foobar', template: \`
\` }) - class Test { + export class Test { set bar() { } }`; @@ -623,6 +660,7 @@ describe('no-access-missing-member', () => { it('should work with getters', () => { let source = ` + @Component({ template: \` \` }) - class Test { + export class Test { foo = []; }`; assertSuccess('no-access-missing-member', source); @@ -639,6 +677,7 @@ describe('no-access-missing-member', () => { it('should work with local template variables', () => { let source = ` + @Component({ template: \` \` }) - class Test { + export class Test { handler() {} }`; assertSuccess('no-access-missing-member', source); @@ -654,10 +693,11 @@ describe('no-access-missing-member', () => { it('should work with array element access', () => { let source = ` + @Component({ template: '{{ names[0].firstName }}' }) - class Test { + export class Test { get names() { return [{ firstName: 'foo' }]; } @@ -667,10 +707,11 @@ describe('no-access-missing-member', () => { it('should fail with array element access', () => { let source = ` + @Component({ template: '{{t.errorData.errorMessages[0].message}}' }) - class Test { + export class Test { get names() { return [{ firstName: 'foo' }]; } @@ -678,11 +719,11 @@ describe('no-access-missing-member', () => { assertFailure('no-access-missing-member', source, { message: 'The property "t" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 2, + line: 3, character: 23 }, endPosition: { - line: 2, + line: 3, character: 24 } }); @@ -690,10 +731,11 @@ describe('no-access-missing-member', () => { it('should fail with array element access', () => { let source = ` + @Component({ template: '{{t.errorData[0].errorMessages.message}}' }) - class Test { + export class Test { get names() { return [{ firstName: 'foo' }]; } @@ -701,11 +743,11 @@ describe('no-access-missing-member', () => { assertFailure('no-access-missing-member', source, { message: 'The property "t" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 2, + line: 3, character: 23 }, endPosition: { - line: 2, + line: 3, character: 24 } }); @@ -713,10 +755,11 @@ describe('no-access-missing-member', () => { it('should succeed with array element access', () => { let source = ` + @Component({ template: '{{t.errorData[0].errorMessages.message}}' }) - class Test { + export class Test { get t() { return [{ firstName: 'foo' }]; } @@ -726,19 +769,20 @@ describe('no-access-missing-member', () => { it('should succeed with array element access', () => { let source = ` + @Component({ template: '
' }) - class Test { + export class Test { }`; assertFailure('no-access-missing-member', source, { message: 'The property "context" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 2, + line: 3, character: 21 }, endPosition: { - line: 2, + line: 3, character: 28 } }); @@ -747,21 +791,22 @@ describe('no-access-missing-member', () => { it('should succeed with array element access', () => { let source = ` + @Component({ template: \`
\` }) - class Test { + export class Test { }`; assertFailure('no-access-missing-member', source, { message: 'The property "context" that you\'re trying to access does not exist in the class declaration.', startPosition: { - line: 2, + line: 3, character: 21 }, endPosition: { - line: 2, + line: 3, character: 28 } }); @@ -781,14 +826,30 @@ describe('no-access-missing-member', () => { // assertFailure('no-access-missing-member', source, { // message: 'The property "bar" that you\'re trying to access does not exist in the class declaration.', // startPosition: { -// line: 3, +// line: 4, // character: 29 // }, // endPosition: { -// line: 3, +// line: 4, // character: 32 // } // }); // }); }); + + describe('extended classes', () => { + it('should properly find the properties of base classes', () => { + let source = ` + class Base { + foo: boolean; + } + + @Component({ + template: '{{foo}}' + }) + export class Test extends Base {} + `; + assertSuccess('no-access-missing-member', source); + }); + }); }); diff --git a/test/testHelper.ts b/test/testHelper.ts index 464d9eb70..ae5dae124 100644 --- a/test/testHelper.ts +++ b/test/testHelper.ts @@ -1,5 +1,109 @@ +import * as ts from 'typescript'; import * as tslint from 'tslint'; import chai = require('chai'); +import {dirname} from 'path'; +import {readFileSync, existsSync} from 'fs'; + +export type File = string; +export type Path = string; +export type Project = {[key: string]: string}; + +const moduleCache = new Map(); +const existenceCache = new Map(); + +const isNodeModule = (filename: string) => filename.startsWith('node_module'); + +export class CompilerHost implements ts.CompilerHost { + private currentDir = ''; + + constructor(public project: Project, private options: ts.CompilerOptions) {} + + useCaseSensitiveFileNames() { + return true; + } + + getNewLine() { + return '\n'; + } + + getSourceFile(filename: string, version: ts.ScriptTarget) { + return ts.createSourceFile(filename, this.readFile(filename), version, true); + } + + readFile(filename: string) { + if (isNodeModule(filename) || filename === this.getDefaultLibFileName()) { + return ''; + } + return this.project[filename]; + } + + fileExists(filename: string) { + if (isNodeModule(filename)) { + return true; + } + return !!this.project[filename]; + } + + getDefaultLibFileName() { + return ts.getDefaultLibFileName(this.options); + } + + getCompilationSettings() { + return this.options; + } + + getCurrentDirectory() { + return this.currentDir; + } + + getScriptFileNames() { + const iter = Object.keys(this.project); + const result = []; + for (let file of iter) { + result.push(file); + } + return result; + } + + writeFile(file: string, content: string) { + this.project[file] = content; + } + + getScriptSnapshot(name: string) { + const content = this.readFile(name); + return ts.ScriptSnapshot.fromString(content); + } + + getDirectories() { + return []; + } + + getCanonicalFileName(name: string) { + return name; + } +} + +export const getProgram = (project: Project, config: any, root: string = ''): ts.Program => { + const filenames = Object.keys(project); + // Any because of different APIs in TypeScript 2.1 and 2.0 + const parseConfigHost: any = { + fileExists: (path: string) => true, + readFile: (file) => null, + readDirectory: (dir: string) => [], + useCaseSensitiveFileNames: true, + }; + const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, root); + parsed.options.baseUrl = parsed.options.baseUrl || root; + parsed.options.allowJs = true; + parsed.options.target = ts.ScriptTarget.ES5; + parsed.options.module = ts.ModuleKind.CommonJS; + parsed.options.declaration = false; + parsed.options.emitDecoratorMetadata = true; + parsed.options.experimentalDecorators = true; + const host = new CompilerHost(project, parsed.options); + const program = ts.createProgram(filenames, parsed.options, host); + return program; +}; interface ISourcePosition { line: number; @@ -27,8 +131,10 @@ function lint(ruleName: string, source: string, options): tslint.LintResult { formattersDirectory: null, fix: false }; - - let linter = new tslint.Linter(linterOptions, undefined); + let project: Project = { + 'file.ts': source + }; + let linter: tslint.Linter = new tslint.Linter(linterOptions, getProgram(project, {})); linter.lint('file.ts', source, configuration); return linter.getResult(); } @@ -58,7 +164,8 @@ export function assertFailures(ruleName: string, source: string, fails: IExpecte chai.assert(result.failureCount > 0, 'no failures'); result.failures.forEach((ruleFail,index) => { chai.assert.equal(fails[index].message, ruleFail.getFailure(), 'error messages dont\'t match'); - chai.assert.deepEqual(fails[index].startPosition, ruleFail.getStartPosition().getLineAndCharacter(), 'start char doesn\'t match'); + chai.assert.deepEqual(fails[index].startPosition, ruleFail.getStartPosition().getLineAndCharacter(), + 'start char doesn\'t match'); chai.assert.deepEqual(fails[index].endPosition, ruleFail.getEndPosition().getLineAndCharacter(), 'end char doesn\'t match'); }); };