From d10e9809531b6795a6ab403f87b5f68b05c8afb6 Mon Sep 17 00:00:00 2001 From: mgechev Date: Sun, 5 Mar 2017 15:37:58 -0800 Subject: [PATCH] fix: proper hostbinding handling --- package.json | 3 + .../templates/basicTemplateAstVisitor.ts | 19 ++- test/noAccessMissingMemberRule.spec.ts | 137 +++++++++++++++++- yarn.lock | 26 +++- 4 files changed, 171 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index ef7d72573..97e090015 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,12 @@ }, "homepage": "https://github.com/mgechev/codelyzer#readme", "devDependencies": { + "@angular/common": "^4.0.0-rc.1", "@angular/compiler": "^4.0.0-rc.1", "@angular/core": "^4.0.0-rc.1", "@angular/forms": "^4.0.0-rc.1", + "@angular/platform-browser": "^4.0.0-rc.1", + "@angular/router": "^4.0.0-rc.1", "@types/chai": "^3.4.33", "@types/less": "0.0.31", "@types/mocha": "^2.2.32", diff --git a/src/angular/templates/basicTemplateAstVisitor.ts b/src/angular/templates/basicTemplateAstVisitor.ts index a712ed0f5..7c7c471ad 100644 --- a/src/angular/templates/basicTemplateAstVisitor.ts +++ b/src/angular/templates/basicTemplateAstVisitor.ts @@ -75,7 +75,7 @@ export interface TemplateAstVisitorCtr { } export class BasicTemplateAstVisitor extends SourceMappingVisitor implements ast.TemplateAstVisitor { - private _variables = []; + protected _variables = []; constructor(sourceFile: ts.SourceFile, private _originalOptions: Lint.IOptions, @@ -101,21 +101,21 @@ export class BasicTemplateAstVisitor extends SourceMappingVisitor implements ast visitNgContent(ast: ast.NgContentAst, context: any): any {} visitEmbeddedTemplate(ast: ast.EmbeddedTemplateAst, context: any): any { + ast.directives.forEach(d => this.visit(d, context)); ast.variables.forEach(v => this.visit(v, context)); ast.children.forEach(e => this.visit(e, context)); ast.outputs.forEach(o => this.visit(o, context)); ast.attrs.forEach(a => this.visit(a, context)); ast.references.forEach(r => this.visit(r, context)); - ast.directives.forEach(d => this.visit(d, context)); } visitElement(element: ast.ElementAst, context: any): any { + element.directives.forEach(d => this.visit(d, context)); element.references.forEach(r => this.visit(r, context)); element.inputs.forEach(i => this.visit(i, context)); element.outputs.forEach(o => this.visit(o, context)); element.attrs.forEach(a => this.visit(a, context)); element.children.forEach(e => this.visit(e, context)); - element.directives.forEach(d => this.visit(d, context)); } visitReference(ast: ast.ReferenceAst, context: any): any { @@ -144,7 +144,6 @@ export class BasicTemplateAstVisitor extends SourceMappingVisitor implements ast visitBoundText(text: ast.BoundTextAst, context: any): any { if (ExpTypes.ASTWithSource(text.value)) { - // Note that will not be reliable for different interpolation symbols const ast: any = (text.value).ast; ast.interpolateExpression = (text.value).source; this.visitNg2TemplateAST(ast, @@ -156,8 +155,16 @@ export class BasicTemplateAstVisitor extends SourceMappingVisitor implements ast visitDirective(ast: ast.DirectiveAst, context: any): any { ast.inputs.forEach(o => this.visit(o, context)); - ast.hostProperties.forEach(p => this.visit(p, context)); - ast.hostEvents.forEach(e => this.visit(e, context)); + const bindingCollector = p => { + if (p.value) { + const val = p.value as any; + if (typeof val.source === 'string') { + this._variables.push(val.source.split('.')[0]); + } + } + }; + ast.hostProperties.forEach(bindingCollector); + ast.hostEvents.forEach(bindingCollector); } visitDirectiveProperty(prop: ast.BoundDirectivePropertyAst, context: any): any { diff --git a/test/noAccessMissingMemberRule.spec.ts b/test/noAccessMissingMemberRule.spec.ts index 227357142..33aae0546 100644 --- a/test/noAccessMissingMemberRule.spec.ts +++ b/test/noAccessMissingMemberRule.spec.ts @@ -407,7 +407,6 @@ describe('no-access-missing-member', () => { }); }); - it('should not throw when template ref used outside component scope', () => { let source = ` import { Component, NgModule } from '@angular/core'; @@ -435,6 +434,7 @@ describe('no-access-missing-member', () => { it('should not throw when routerLinkActive template ref is used in component', () => { let source = ` import { Component, NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; @Component({ selector: 'foobar', @@ -443,6 +443,7 @@ describe('no-access-missing-member', () => { export class Test {} @NgModule({ + imports: [RouterModule.forRoot([])], declarations: [Test], exports: [Test] }) @@ -451,7 +452,6 @@ describe('no-access-missing-member', () => { assertSuccess('no-access-missing-member', source); }); - it('should not throw when ngModel template ref is used in component', () => { let source = ` import { Component, NgModule } from '@angular/core'; @@ -528,6 +528,42 @@ describe('no-access-missing-member', () => { assertSuccess('no-access-missing-member', source); }); + it('should fail with nonexisting @HostBinding', () => { + let source = ` + import { Directive, HostBinding, Component, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + @Directive({ + selector: '[foo]' + }) + export class FooDirective {} + + @Component({ + template: \`
\` + }) + export class Test { + } + + @NgModule({ + imports: [CommonModule], + declarations: [Test, FooDirective], + exports: [Test] + }) + export class MainModule {} + `; + 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: 10, + character: 44 + }, + endPosition: { + line: 10, + character: 47 + } + }); + }); + }); describe('valid expressions', () => { @@ -649,6 +685,99 @@ describe('no-access-missing-member', () => { `; assertSuccess('no-access-missing-member', source); }); + + it('should succeed with existing @HostBinding', () => { + let source = ` + import { Directive, HostBinding, Component, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + @Directive({ + selector: '[foo]', + host: { + '[attr.title]': 'foo' + } + }) + export class FooDirective { + foo = 42; + } + + @Component({ + template: \`
\` + }) + export class Test { + } + + @NgModule({ + imports: [CommonModule], + declarations: [Test, FooDirective], + exports: [Test] + }) + export class MainModule {} + `; + assertSuccess('no-access-missing-member', source); + }); + + it('should succeed with existing @HostBinding', () => { + let source = ` + import { Directive, HostBinding, Component, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + @Directive({ + selector: '[foo]', + host: { + '[attr.title]': 'foo.bar' + } + }) + export class FooDirective { + foo = { + bar: 42 + }; + } + + @Component({ + template: \`
\` + }) + export class Test { + } + + @NgModule({ + imports: [CommonModule], + declarations: [Test, FooDirective], + exports: [Test] + }) + export class MainModule {} + `; + assertSuccess('no-access-missing-member', source); + }); + + it('should succeed with existing @HostBinding', () => { + let source = ` + import { Directive, HostBinding, Component, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + @Directive({ + selector: '[foo]' + }) + export class FooDirective { + @HostBinding('attr.title') foo = 42; + } + + @Component({ + template: \`
\` + }) + export class Test { + } + + @NgModule({ + imports: [CommonModule], + declarations: [Test, FooDirective], + exports: [Test] + }) + export class MainModule {} + `; + assertSuccess('no-access-missing-member', source); + }); + }); describe('nested properties and pipes', () => { @@ -1017,8 +1146,7 @@ describe('no-access-missing-member', () => { @Component({ template: '
' }) - export class Test { - } + export class Test {} @NgModule({ imports: [CommonModule], @@ -1040,7 +1168,6 @@ describe('no-access-missing-member', () => { }); }); - it('should succeed with array element access', () => { let source = ` import { Component, NgModule } from '@angular/core'; diff --git a/yarn.lock b/yarn.lock index 277ff269b..459b0a115 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@angular/common@^4.0.0-rc.1": + version "4.0.0-rc.2" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.0.0-rc.2.tgz#69f68639270d71b2e8c552e4fa939975fcb88304" + "@angular/compiler-cli@^4.0.0-rc.1": version "4.0.0-rc.1" resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.0.0-rc.1.tgz#48cdcfb691eac2152602f296fb9fa7ffc4bfa5bd" @@ -18,6 +22,18 @@ version "4.0.0-rc.1" resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.0.0-rc.1.tgz#7f87b7696b407476e45d6d3c1880a50d5afbb6e3" +"@angular/forms@^4.0.0-rc.1": + version "4.0.0-rc.2" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.0.0-rc.2.tgz#6d9df97783b6023d652d97369db13d6ad6c7fa9e" + +"@angular/platform-browser@^4.0.0-rc.1": + version "4.0.0-rc.2" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.0.0-rc.2.tgz#bcca05ce85d320ee0b257640f15479b59fed20f0" + +"@angular/router@^4.0.0-rc.1": + version "4.0.0-rc.2" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-4.0.0-rc.2.tgz#66fc5be012caa38441314d0a0b9c9b6a723c471a" + "@angular/tsc-wrapped@4.0.0-rc.1": version "4.0.0-rc.1" resolved "https://registry.yarnpkg.com/@angular/tsc-wrapped/-/tsc-wrapped-4.0.0-rc.1.tgz#767ce64d70c66b9d4bd8ba1ef99a47bc02836336" @@ -46,6 +62,10 @@ version "6.0.63" resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.63.tgz#e08acbbd5946e0e95990b1c76f3ce5b7882a48eb" +"@types/rimraf@0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-0.0.28.tgz#5562519bc7963caca8abf7f128cae3b594d41d06" + "@types/source-map@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@types/source-map/-/source-map-0.5.0.tgz#dd34bbd8e32fe4e74f2e3d8ac07f8aa5b45a47ac" @@ -973,9 +993,9 @@ nan@^2.3.2: version "2.5.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" -ngast@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/ngast/-/ngast-0.0.1.tgz#a63f599dbc8270773f68d4afd9d10397515d2db3" +ngast@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/ngast/-/ngast-0.0.2.tgz#faf989566b0ff608dbbe427f0cd79d77ad92836a" node-gyp@^3.3.1: version "3.5.0"