Ayaz Hafiz 859ebdd836 fix(ivy): correctly bind targetToIdentifier to the TemplateVisitor (#31861)
`TemplateVisitor#visitBoundAttribute` currently has to invoke visiting
expressions manually (this is fixed in #31813). Previously, it did not
bind `targetToIdentifier` to the visitor before deferring to the
expression visitor, which breaks the `targetToIdentifier` code. This
fixes that and adds a test to ensure the closure processed correctly.

This change is urgent; without it, many indexing targets in g3 are
broken.

PR Close #31861
2019-07-26 12:03:16 -07:00

761 lines
25 KiB
TypeScript

/**
* @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 {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind, ReferenceIdentifier, TemplateNodeIdentifier, TopLevelIdentifier, VariableIdentifier} from '..';
import {runInEachFileSystem} from '../../file_system/testing';
import {getTemplateIdentifiers} from '../src/template';
import * as util from './util';
function bind(template: string) {
return util.getBoundTemplate(template, {
preserveWhitespaces: true,
leadingTriviaChars: [],
});
}
runInEachFileSystem(() => {
describe('getTemplateIdentifiers', () => {
it('should generate nothing in empty template', () => {
const template = '';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(0);
});
it('should ignore comments', () => {
const template = '<!-- {{comment}} -->';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(0);
});
it('should handle arbitrary whitespace', () => {
const template = '\n\n {{foo}}';
const refs = getTemplateIdentifiers(bind(template));
const [ref] = Array.from(refs);
expect(ref).toEqual({
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(7, 10),
target: null,
});
});
it('should resist collisions', () => {
const template = '<div [bar]="bar ? bar : bar"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
{
name: 'bar',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
target: null,
},
{
name: 'bar',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(18, 21),
target: null,
},
{
name: 'bar',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(24, 27),
target: null,
},
] as TopLevelIdentifier[]));
});
describe('generates identifiers for PropertyReads', () => {
it('should discover component properties', () => {
const template = '{{foo}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref).toEqual({
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
target: null,
});
});
it('should discover nested properties', () => {
const template = '<div><span>{{foo}}</span></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(13, 16),
target: null,
});
});
it('should ignore identifiers that are not implicitly received by the template', () => {
const template = '{{foo.bar.baz}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
});
it('should discover properties in bound attributes', () => {
const template = '<div [bar]="bar"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'bar',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
target: null,
});
});
it('should discover variables in bound attributes', () => {
const template = '<div #div [value]="div.innerText"></div>';
const refs = getTemplateIdentifiers(bind(template));
const elementReference: ElementIdentifier = {
name: 'div',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 4),
attributes: new Set(),
usedDirectives: new Set(),
};
const reference: ReferenceIdentifier = {
name: 'div',
kind: IdentifierKind.Reference,
span: new AbsoluteSourceSpan(6, 9),
target: {node: elementReference, directive: null},
};
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'div',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(19, 22),
target: reference,
});
});
it('should discover properties in template expressions', () => {
const template = '<div [bar]="bar ? bar1 : bar2"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
{
name: 'bar',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
target: null,
},
{
name: 'bar1',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(18, 22),
target: null,
},
{
name: 'bar2',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(25, 29),
target: null,
},
] as TopLevelIdentifier[]));
});
it('should discover properties in template expressions', () => {
const template = '<div *ngFor="let foo of foos"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'foos',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(24, 28),
target: null,
});
});
});
describe('generates identifiers for PropertyWrites', () => {
it('should discover property writes in bound events', () => {
const template = '<div (click)="foo=bar"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(14, 17),
target: null,
},
{
name: 'bar',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(18, 21),
target: null,
}
] as TopLevelIdentifier[]));
});
it('should discover nested property writes', () => {
const template = '<div><span (click)="foo=bar"></span></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(20, 23),
target: null,
}] as TopLevelIdentifier[]));
});
it('should ignore property writes that are not implicitly received by the template', () => {
const template = '<div><span (click)="foo.bar=baz"></span></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
const bar = refArr.find(ref => ref.name.includes('bar'));
expect(bar).toBeUndefined();
});
});
describe('generates identifiers for MethodCalls', () => {
it('should discover component method calls', () => {
const template = '{{foo()}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref).toEqual({
name: 'foo',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(2, 5),
target: null,
});
});
it('should discover nested properties', () => {
const template = '<div><span>{{foo()}}</span></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'foo',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(13, 16),
target: null,
});
});
it('should ignore identifiers that are not implicitly received by the template', () => {
const template = '{{foo().bar().baz()}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
});
it('should discover method calls in bound attributes', () => {
const template = '<div [bar]="bar()"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'bar',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(12, 15),
target: null,
});
});
it('should discover method calls in template expressions', () => {
const template = '<div *ngFor="let foo of foos()"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'foos',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(24, 28),
target: null,
});
});
});
});
describe('generates identifiers for template reference variables', () => {
it('should discover references', () => {
const template = '<div #foo>';
const refs = getTemplateIdentifiers(bind(template));
const elementReference: ElementIdentifier = {
name: 'div',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 4),
attributes: new Set(),
usedDirectives: new Set(),
};
const refArray = Array.from(refs);
expect(refArray).toEqual(jasmine.arrayContaining([{
name: 'foo',
kind: IdentifierKind.Reference,
span: new AbsoluteSourceSpan(6, 9),
target: {node: elementReference, directive: null},
}] as TopLevelIdentifier[]));
});
it('should discover nested references', () => {
const template = '<div><span #foo></span></div>';
const refs = getTemplateIdentifiers(bind(template));
const elementReference: ElementIdentifier = {
name: 'span',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(6, 10),
attributes: new Set(),
usedDirectives: new Set(),
};
const refArray = Array.from(refs);
expect(refArray).toEqual(jasmine.arrayContaining([{
name: 'foo',
kind: IdentifierKind.Reference,
span: new AbsoluteSourceSpan(12, 15),
target: {node: elementReference, directive: null},
}] as TopLevelIdentifier[]));
});
it('should discover references to references', () => {
const template = `<div #foo>{{foo.className}}</div>`;
const refs = getTemplateIdentifiers(bind(template));
const elementIdentifier: ElementIdentifier = {
name: 'div',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 4),
attributes: new Set(),
usedDirectives: new Set(),
};
const referenceIdentifier: ReferenceIdentifier = {
name: 'foo',
kind: IdentifierKind.Reference,
span: new AbsoluteSourceSpan(6, 9),
target: {node: elementIdentifier, directive: null},
};
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
elementIdentifier, referenceIdentifier, {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
target: referenceIdentifier,
}
] as TopLevelIdentifier[]));
});
it('should discover forward references', () => {
const template = `{{foo}}<div #foo></div>`;
const refs = getTemplateIdentifiers(bind(template));
const elementIdentifier: ElementIdentifier = {
name: 'div',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(8, 11),
attributes: new Set(),
usedDirectives: new Set(),
};
const referenceIdentifier: ReferenceIdentifier = {
name: 'foo',
kind: IdentifierKind.Reference,
span: new AbsoluteSourceSpan(13, 16),
target: {node: elementIdentifier, directive: null},
};
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
elementIdentifier, referenceIdentifier, {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
target: referenceIdentifier,
}
] as TopLevelIdentifier[]));
});
it('should generate information directive targets', () => {
const declB = util.getComponentDeclaration('class B {}', 'B');
const template = '<div #foo b-selector>';
const boundTemplate = util.getBoundTemplate(template, {}, [
{selector: '[b-selector]', declaration: declB},
]);
const refs = getTemplateIdentifiers(boundTemplate);
const refArr = Array.from(refs);
let fooRef = refArr.find(id => id.name === 'foo');
expect(fooRef).toBeDefined();
expect(fooRef !.kind).toBe(IdentifierKind.Reference);
fooRef = fooRef as ReferenceIdentifier;
expect(fooRef.target).toBeDefined();
expect(fooRef.target !.node.kind).toBe(IdentifierKind.Element);
expect(fooRef.target !.node.name).toBe('div');
expect(fooRef.target !.node.span).toEqual(new AbsoluteSourceSpan(1, 4));
expect(fooRef.target !.directive).toEqual(declB);
});
it('should discover references to references', () => {
const template = `<div #foo (ngSubmit)="do(foo)"></div>`;
const refs = getTemplateIdentifiers(bind(template));
const elementIdentifier: ElementIdentifier = {
name: 'div',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 4),
attributes: new Set(),
usedDirectives: new Set(),
};
const referenceIdentifier: ReferenceIdentifier = {
name: 'foo',
kind: IdentifierKind.Reference,
span: new AbsoluteSourceSpan(6, 9),
target: {node: elementIdentifier, directive: null},
};
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
elementIdentifier, referenceIdentifier, {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(25, 28),
target: referenceIdentifier,
}
] as TopLevelIdentifier[]));
});
});
describe('generates identifiers for template variables', () => {
it('should discover variables', () => {
const template = '<div *ngFor="let foo of foos">';
const refs = getTemplateIdentifiers(bind(template));
const refArray = Array.from(refs);
expect(refArray).toEqual(jasmine.arrayContaining([{
name: 'foo',
kind: IdentifierKind.Variable,
span: new AbsoluteSourceSpan(17, 20),
}] as TopLevelIdentifier[]));
});
it('should discover variables with let- syntax', () => {
const template = '<ng-template let-var="classVar">';
const refs = getTemplateIdentifiers(bind(template));
const refArray = Array.from(refs);
expect(refArray).toEqual(jasmine.arrayContaining([{
name: 'var',
kind: IdentifierKind.Variable,
span: new AbsoluteSourceSpan(17, 20),
}] as TopLevelIdentifier[]));
});
it('should discover nested variables', () => {
const template = '<div><span *ngFor="let foo of foos"></span></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArray = Array.from(refs);
expect(refArray).toEqual(jasmine.arrayContaining([{
name: 'foo',
kind: IdentifierKind.Variable,
span: new AbsoluteSourceSpan(23, 26),
}] as TopLevelIdentifier[]));
});
it('should discover references to variables', () => {
const template = `<div *ngFor="let foo of foos; let i = index">{{foo + i}}</div>`;
const refs = getTemplateIdentifiers(bind(template));
const fooIdentifier: VariableIdentifier = {
name: 'foo',
kind: IdentifierKind.Variable,
span: new AbsoluteSourceSpan(17, 20),
};
const iIdentifier: VariableIdentifier = {
name: 'i',
kind: IdentifierKind.Variable,
span: new AbsoluteSourceSpan(34, 35),
};
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
fooIdentifier,
{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(47, 50),
target: fooIdentifier,
},
iIdentifier,
{
name: 'i',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(53, 54),
target: iIdentifier,
},
] as TopLevelIdentifier[]));
});
it('should discover references to variables', () => {
const template = `<div *ngFor="let foo of foos" (click)="do(foo)"></div>`;
const refs = getTemplateIdentifiers(bind(template));
const variableIdentifier: VariableIdentifier = {
name: 'foo',
kind: IdentifierKind.Variable,
span: new AbsoluteSourceSpan(17, 20),
};
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([
variableIdentifier, {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(42, 45),
target: variableIdentifier,
}
] as TopLevelIdentifier[]));
});
});
describe('generates identifiers for elements', () => {
it('should record elements as ElementIdentifiers', () => {
const template = '<test-selector>';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref.kind).toBe(IdentifierKind.Element);
});
it('should record element names as their selector', () => {
const template = '<test-selector>';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref as ElementIdentifier).toEqual({
name: 'test-selector',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 14),
attributes: new Set(),
usedDirectives: new Set(),
});
});
it('should discover selectors in self-closing elements', () => {
const template = '<img />';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref as ElementIdentifier).toEqual({
name: 'img',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 4),
attributes: new Set(),
usedDirectives: new Set(),
});
});
it('should discover selectors in elements with adjacent open and close tags', () => {
const template = '<test-selector></test-selector>';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref as ElementIdentifier).toEqual({
name: 'test-selector',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 14),
attributes: new Set(),
usedDirectives: new Set(),
});
});
it('should discover selectors in elements with non-adjacent open and close tags', () => {
const template = '<test-selector> text </test-selector>';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref as ElementIdentifier).toEqual({
name: 'test-selector',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(1, 14),
attributes: new Set(),
usedDirectives: new Set(),
});
});
it('should discover nested selectors', () => {
const template = '<div><span></span></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'span',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(6, 10),
attributes: new Set(),
usedDirectives: new Set(),
});
});
it('should generate information about attributes', () => {
const template = '<div attrA attrB="val"></div>';
const refs = getTemplateIdentifiers(bind(template));
const [ref] = Array.from(refs);
const attrs = (ref as ElementIdentifier).attributes;
expect(attrs).toEqual(new Set<AttributeIdentifier>([
{
name: 'attrA',
kind: IdentifierKind.Attribute,
span: new AbsoluteSourceSpan(5, 10),
},
{
name: 'attrB',
kind: IdentifierKind.Attribute,
span: new AbsoluteSourceSpan(11, 22),
}
]));
});
it('should generate information about used directives', () => {
const declA = util.getComponentDeclaration('class A {}', 'A');
const declB = util.getComponentDeclaration('class B {}', 'B');
const declC = util.getComponentDeclaration('class C {}', 'C');
const template = '<a-selector b-selector></a-selector>';
const boundTemplate = util.getBoundTemplate(template, {}, [
{selector: 'a-selector', declaration: declA},
{selector: '[b-selector]', declaration: declB},
{selector: ':not(never-selector)', declaration: declC},
]);
const refs = getTemplateIdentifiers(boundTemplate);
const [ref] = Array.from(refs);
const usedDirectives = (ref as ElementIdentifier).usedDirectives;
expect(usedDirectives).toEqual(new Set([
{
node: declA,
selector: 'a-selector',
},
{
node: declB,
selector: '[b-selector]',
},
{
node: declC,
selector: ':not(never-selector)',
}
]));
});
});
describe('generates identifiers for templates', () => {
it('should record templates as TemplateNodeIdentifiers', () => {
const template = '<ng-template>';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref.kind).toBe(IdentifierKind.Template);
});
it('should record template names as their tag name', () => {
const template = '<ng-template>';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref as TemplateNodeIdentifier).toEqual({
name: 'ng-template',
kind: IdentifierKind.Template,
span: new AbsoluteSourceSpan(1, 12),
attributes: new Set(),
usedDirectives: new Set(),
});
});
it('should discover nested templates', () => {
const template = '<div><ng-template></ng-template></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
expect(refArr).toContain({
name: 'ng-template',
kind: IdentifierKind.Template,
span: new AbsoluteSourceSpan(6, 17),
attributes: new Set(),
usedDirectives: new Set(),
});
});
it('should generate information about attributes', () => {
const template = '<ng-template attrA attrB="val">';
const refs = getTemplateIdentifiers(bind(template));
const [ref] = Array.from(refs);
const attrs = (ref as TemplateNodeIdentifier).attributes;
expect(attrs).toEqual(new Set<AttributeIdentifier>([
{
name: 'attrA',
kind: IdentifierKind.Attribute,
span: new AbsoluteSourceSpan(13, 18),
},
{
name: 'attrB',
kind: IdentifierKind.Attribute,
span: new AbsoluteSourceSpan(19, 30),
}
]));
});
it('should generate information about used directives', () => {
const declB = util.getComponentDeclaration('class B {}', 'B');
const declC = util.getComponentDeclaration('class C {}', 'C');
const template = '<ng-template b-selector>';
const boundTemplate = util.getBoundTemplate(template, {}, [
{selector: '[b-selector]', declaration: declB},
{selector: ':not(never-selector)', declaration: declC},
]);
const refs = getTemplateIdentifiers(boundTemplate);
const [ref] = Array.from(refs);
const usedDirectives = (ref as ElementIdentifier).usedDirectives;
expect(usedDirectives).toEqual(new Set([
{
node: declB,
selector: '[b-selector]',
},
{
node: declC,
selector: ':not(never-selector)',
}
]));
});
});
});