refactor(compiler): simplify visitor logic for attributes (#38899)

The logic for computing identifiers, specifically for bound attributes
can be simplified by using the value span of the binding rather than the
source span.

PR Close #38899
This commit is contained in:
Andrew Scott 2020-09-17 16:16:17 -07:00 committed by Misko Hevery
parent dfbfabc052
commit 0c0c54d615
2 changed files with 52 additions and 25 deletions

View File

@ -97,15 +97,12 @@ class ExpressionVisitor extends RecursiveAstVisitor {
return; return;
} }
// Get the location of the identifier of real interest. // The source span of the requested AST starts at a location that is offset from the expression.
// The compiler's expression parser records the location of some expressions in a manner not const identifierStart = ast.sourceSpan.start - this.absoluteOffset;
// useful to the indexer. For example, a `MethodCall` `foo(a, b)` will record the span of the if (!this.expressionStr.substring(identifierStart).startsWith(ast.name)) {
// entire method call, but the indexer is interested only in the method identifier. throw new Error(`Impossible state: "${ast.name}" not found in "${
const localExpression = this.expressionStr.substr(ast.span.start); this.expressionStr}" at location ${identifierStart}`);
if (!localExpression.includes(ast.name)) {
throw new Error(`Impossible state: "${ast.name}" not found in "${localExpression}"`);
} }
const identifierStart = ast.span.start + localExpression.indexOf(ast.name);
// Join the relative position of the expression within a node with the absolute position // Join the relative position of the expression within a node with the absolute position
// of the node to get the absolute position of the expression in the source code. // of the node to get the absolute position of the expression in the source code.
@ -191,25 +188,14 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
this.visitAll(template.references); this.visitAll(template.references);
} }
visitBoundAttribute(attribute: TmplAstBoundAttribute) { visitBoundAttribute(attribute: TmplAstBoundAttribute) {
// A BoundAttribute's value (the parent AST) may have subexpressions (children ASTs) that have // If the bound attribute has no value, it cannot have any identifiers in the value expression.
// recorded spans extending past the recorded span of the parent. The most common example of if (attribute.valueSpan === undefined) {
// this is with `*ngFor`. return;
// To resolve this, use the information on the BoundAttribute Template AST, which is always }
// correct, to determine locations of identifiers in the expression.
//
// TODO(ayazhafiz): Remove this when https://github.com/angular/angular/pull/31813 lands.
const attributeSrc = attribute.sourceSpan.toString();
const attributeAbsolutePosition = attribute.sourceSpan.start.offset;
// Skip the bytes of the attribute name so that there are no collisions between the attribute
// name and expression identifier names later.
const nameSkipOffet = attributeSrc.indexOf(attribute.name) + attribute.name.length;
const expressionSrc = attributeSrc.substring(nameSkipOffet);
const expressionAbsolutePosition = attributeAbsolutePosition + nameSkipOffet;
const identifiers = ExpressionVisitor.getIdentifiers( const identifiers = ExpressionVisitor.getIdentifiers(
attribute.value, expressionSrc, expressionAbsolutePosition, this.boundTemplate, attribute.value, attribute.valueSpan.toString(), attribute.valueSpan.start.offset,
this.targetToIdentifier.bind(this)); this.boundTemplate, this.targetToIdentifier.bind(this));
identifiers.forEach(id => this.identifiers.add(id)); identifiers.forEach(id => this.identifiers.add(id));
} }
visitBoundEvent(attribute: TmplAstBoundEvent) { visitBoundEvent(attribute: TmplAstBoundEvent) {

View File

@ -124,6 +124,20 @@ runInEachFileSystem(() => {
}); });
}); });
it('should handle bound attributes with no value', () => {
const template = '<div [bar]></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toEqual([{
name: 'div',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 4),
attributes: new Set(),
usedDirectives: new Set(),
}]);
});
it('should discover variables in bound attributes', () => { it('should discover variables in bound attributes', () => {
const template = '<div #div [value]="div.innerText"></div>'; const template = '<div #div [value]="div.innerText"></div>';
const refs = getTemplateIdentifiers(bind(template)); const refs = getTemplateIdentifiers(bind(template));
@ -189,6 +203,33 @@ runInEachFileSystem(() => {
target: null, target: null,
}); });
}); });
it('should discover properties in template expressions and resist collisions', () => {
const template = '<div *ngFor="let foo of (foos ? foos : foos)"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
{
name: 'foos',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(25, 29),
target: null,
},
{
name: 'foos',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(32, 36),
target: null,
},
{
name: 'foos',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(39, 43),
target: null,
},
]));
});
}); });
describe('generates identifiers for PropertyWrites', () => { describe('generates identifiers for PropertyWrites', () => {