From a9242c4fc22f526a3dc592113880d68d7499e91c Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 22 Apr 2019 13:19:23 +0200 Subject: [PATCH] refactor(core): template-var-assignment migration incorrectly warns (#30026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently the `template-var-assignment` migration incorrectly warns if the template writes to a property in the component that has the same `ast.PropertyWrite´ name as a template input variable but different receiver. e.g. ```html ``` Similarly if an output writes to a component property with the same name as a template input variable, but the expression is within a different template scope, the schematic currently incorrectly warns. e.g. ```html ``` PR Close #30026 --- .../analyze_template.ts | 23 ++---- .../html_variable_assignment_visitor.ts | 79 +++++++++++++++++++ .../angular/property_write_html_visitor.ts | 61 -------------- .../template_var_assignment_migration_spec.ts | 49 ++++++++++++ 4 files changed, 134 insertions(+), 78 deletions(-) create mode 100644 packages/core/schematics/migrations/template-var-assignment/angular/html_variable_assignment_visitor.ts delete mode 100644 packages/core/schematics/migrations/template-var-assignment/angular/property_write_html_visitor.ts diff --git a/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts b/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts index c140770232..fbd2b69580 100644 --- a/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts +++ b/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts @@ -7,12 +7,10 @@ */ import {PropertyWrite} from '@angular/compiler'; -import {Variable, visitAll} from '@angular/compiler/src/render3/r3_ast'; - +import {visitAll} from '@angular/compiler/src/render3/r3_ast'; import {ResolvedTemplate} from '../../utils/ng_component_template'; import {parseHtmlGracefully} from '../../utils/parse_html'; - -import {PropertyAssignment, PropertyWriteHtmlVisitor} from './angular/property_write_html_visitor'; +import {HtmlVariableAssignmentVisitor} from './angular/html_variable_assignment_visitor'; export interface TemplateVariableAssignment { node: PropertyWrite; @@ -32,20 +30,11 @@ export function analyzeResolvedTemplate(template: ResolvedTemplate): TemplateVar return null; } - const visitor = new PropertyWriteHtmlVisitor(); + const visitor = new HtmlVariableAssignmentVisitor(); - // Analyze the Angular Render3 HTML AST and collect all property assignments and - // template variables. + // Analyze the Angular Render3 HTML AST and collect all template variable assignments. visitAll(visitor, templateNodes); - return filterTemplateVariableAssignments(visitor.propertyAssignments, visitor.templateVariables) - .map(({node, start, end}) => ({node, start: start + node.span.start, end})); -} - -/** - * Returns all template variable assignments by looking if a given property - * assignment is setting the value for one of the specified template variables. - */ -function filterTemplateVariableAssignments(writes: PropertyAssignment[], variables: Variable[]) { - return writes.filter(propertyWrite => !!variables.find(v => v.name === propertyWrite.node.name)); + return visitor.variableAssignments.map( + ({node, start, end}) => ({node, start: start + node.span.start, end})); } diff --git a/packages/core/schematics/migrations/template-var-assignment/angular/html_variable_assignment_visitor.ts b/packages/core/schematics/migrations/template-var-assignment/angular/html_variable_assignment_visitor.ts new file mode 100644 index 0000000000..07973ef2ee --- /dev/null +++ b/packages/core/schematics/migrations/template-var-assignment/angular/html_variable_assignment_visitor.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ImplicitReceiver, ParseSourceSpan, PropertyWrite, RecursiveAstVisitor} from '@angular/compiler'; +import {BoundEvent, Element, NullVisitor, Template, Variable, visitAll} from '@angular/compiler/src/render3/r3_ast'; + +export interface TemplateVariableAssignment { + start: number; + end: number; + node: PropertyWrite; +} + +/** + * HTML AST visitor that traverses the Render3 HTML AST in order to find all + * expressions that write to local template variables within bound events. + */ +export class HtmlVariableAssignmentVisitor extends NullVisitor { + variableAssignments: TemplateVariableAssignment[] = []; + + private currentVariables: Variable[] = []; + private expressionAstVisitor = + new ExpressionAstVisitor(this.variableAssignments, this.currentVariables); + + visitElement(element: Element): void { + visitAll(this, element.outputs); + visitAll(this, element.children); + } + + visitTemplate(template: Template): void { + // Keep track of the template variables which can be accessed by the template + // child nodes through the implicit receiver. + this.currentVariables.push(...template.variables); + + // Visit all children of the template. The template proxies the outputs of the + // immediate child elements, so we just ignore outputs on the "Template" in order + // to not visit similar bound events twice. + visitAll(this, template.children); + + // Remove all previously added variables since all children that could access + // these have been visited already. + template.variables.forEach(v => { + const variableIdx = this.currentVariables.indexOf(v); + + if (variableIdx !== -1) { + this.currentVariables.splice(variableIdx, 1); + } + }); + } + + visitBoundEvent(node: BoundEvent) { + node.handler.visit(this.expressionAstVisitor, node.handlerSpan); + } +} + +/** AST visitor that resolves all variable assignments within a given expression AST. */ +class ExpressionAstVisitor extends RecursiveAstVisitor { + constructor( + private variableAssignments: TemplateVariableAssignment[], + private currentVariables: Variable[]) { + super(); + } + + visitPropertyWrite(node: PropertyWrite, span: ParseSourceSpan) { + if (node.receiver instanceof ImplicitReceiver && + this.currentVariables.some(v => v.name === node.name)) { + this.variableAssignments.push({ + node: node, + start: span.start.offset, + end: span.end.offset, + }); + } + super.visitPropertyWrite(node, span); + } +} diff --git a/packages/core/schematics/migrations/template-var-assignment/angular/property_write_html_visitor.ts b/packages/core/schematics/migrations/template-var-assignment/angular/property_write_html_visitor.ts deleted file mode 100644 index 333edddb70..0000000000 --- a/packages/core/schematics/migrations/template-var-assignment/angular/property_write_html_visitor.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {ParseSourceSpan, PropertyWrite, RecursiveAstVisitor} from '@angular/compiler'; -import {BoundEvent, Element, NullVisitor, Template, Variable, visitAll} from '@angular/compiler/src/render3/r3_ast'; - -export interface PropertyAssignment { - start: number; - end: number; - node: PropertyWrite; -} - -/** - * AST visitor that traverses the Render3 HTML AST in order to find all declared - * template variables and property assignments within bound events. - */ -export class PropertyWriteHtmlVisitor extends NullVisitor { - templateVariables: Variable[] = []; - propertyAssignments: PropertyAssignment[] = []; - - private expressionAstVisitor = new ExpressionAstVisitor(this.propertyAssignments); - - visitElement(element: Element): void { - visitAll(this, element.outputs); - visitAll(this, element.children); - } - - visitTemplate(template: Template): void { - // Visit all children of the template. The template proxies the outputs of the - // immediate child elements, so we just ignore outputs on the "Template" in order - // to not visit similar bound events twice. - visitAll(this, template.children); - - // Keep track of all declared local template variables. - this.templateVariables.push(...template.variables); - } - - visitBoundEvent(node: BoundEvent) { - node.handler.visit(this.expressionAstVisitor, node.handlerSpan); - } -} - -/** AST visitor that resolves all property assignments with a given expression AST. */ -class ExpressionAstVisitor extends RecursiveAstVisitor { - constructor(private propertyAssignments: PropertyAssignment[]) { super(); } - - visitPropertyWrite(node: PropertyWrite, span: ParseSourceSpan) { - this.propertyAssignments.push({ - node: node, - start: span.start.offset, - end: span.end.offset, - }); - - super.visitPropertyWrite(node, span); - } -} diff --git a/packages/core/schematics/test/template_var_assignment_migration_spec.ts b/packages/core/schematics/test/template_var_assignment_migration_spec.ts index 87a3ba4f9e..810d187c9e 100644 --- a/packages/core/schematics/test/template_var_assignment_migration_spec.ts +++ b/packages/core/schematics/test/template_var_assignment_migration_spec.ts @@ -181,6 +181,55 @@ describe('template variable assignment migration', () => { expect(warnOutput.length).toBe(0); }); + it('should not warn for property writes with template variable name but different receiver', + () => { + writeFile('/index.ts', ` + import {Component} from '@angular/core'; + + @Component({ + templateUrl: './sub_dir/tmpl.html', + }) + export class MyComp { + someProp = { + element: 'someValue', + }; + } + `); + + writeFile('/sub_dir/tmpl.html', ` + + `); + + runMigration(); + + expect(warnOutput.length).toBe(0); + }); + + it('should not warn for property writes with template variable name but different scope', () => { + writeFile('/index.ts', ` + import {Component} from '@angular/core'; + + @Component({ + templateUrl: './sub_dir/tmpl.html', + }) + export class MyComp { + element = 'someValue'; + } + `); + + writeFile('/sub_dir/tmpl.html', ` + + + `); + + runMigration(); + + expect(warnOutput.length).toBe(0); + }); + + it('should not throw an error if a detected template fails parsing', () => { writeFile('/index.ts', ` import {Component} from '@angular/core';