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:
parent
94aeeec1dc
commit
a9242c4fc2
|
@ -7,12 +7,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {PropertyWrite} from '@angular/compiler';
|
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 {ResolvedTemplate} from '../../utils/ng_component_template';
|
||||||
import {parseHtmlGracefully} from '../../utils/parse_html';
|
import {parseHtmlGracefully} from '../../utils/parse_html';
|
||||||
|
import {HtmlVariableAssignmentVisitor} from './angular/html_variable_assignment_visitor';
|
||||||
import {PropertyAssignment, PropertyWriteHtmlVisitor} from './angular/property_write_html_visitor';
|
|
||||||
|
|
||||||
export interface TemplateVariableAssignment {
|
export interface TemplateVariableAssignment {
|
||||||
node: PropertyWrite;
|
node: PropertyWrite;
|
||||||
|
@ -32,20 +30,11 @@ export function analyzeResolvedTemplate(template: ResolvedTemplate): TemplateVar
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visitor = new PropertyWriteHtmlVisitor();
|
const visitor = new HtmlVariableAssignmentVisitor();
|
||||||
|
|
||||||
// Analyze the Angular Render3 HTML AST and collect all property assignments and
|
// Analyze the Angular Render3 HTML AST and collect all template variable assignments.
|
||||||
// template variables.
|
|
||||||
visitAll(visitor, templateNodes);
|
visitAll(visitor, templateNodes);
|
||||||
|
|
||||||
return filterTemplateVariableAssignments(visitor.propertyAssignments, visitor.templateVariables)
|
return visitor.variableAssignments.map(
|
||||||
.map(({node, start, end}) => ({node, start: start + node.span.start, end}));
|
({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));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -181,6 +181,55 @@ describe('template variable assignment migration', () => {
|
||||||
expect(warnOutput.length).toBe(0);
|
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', `
|
||||||
|
<button *ngFor="let element of list" (click)="someProp.element = null">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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', `
|
||||||
|
<button *ngFor="let element of list">{{element}}</button>
|
||||||
|
<button (click)="element = null"></button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
||||||
|
expect(warnOutput.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should not throw an error if a detected template fails parsing', () => {
|
it('should not throw an error if a detected template fails parsing', () => {
|
||||||
writeFile('/index.ts', `
|
writeFile('/index.ts', `
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
|
|
Loading…
Reference in New Issue