refactor(core): template-var-assignment migration incorrectly warns (#30026)

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
<!-- "someProp.element" will be incorrectly reported as template variable assignment -->
<button *ngFor="let element of list" (click)="someProp.element = null">Reset</button>
```

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
<button *ngFor="let element of list">{{element}}</button>

<!-- The "element = null" expression does not refer to the "element" template input variable -->
<button (click)="element = null"></button>
```

PR Close #30026
This commit is contained in:
Paul Gschwendtner
2019-04-22 13:19:23 +02:00
committed by Ben Lesh
parent 94aeeec1dc
commit a9242c4fc2
4 changed files with 134 additions and 78 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}