feat(ivy): index template references, variables, bound attributes/events (#31535)

Adds support for indexing template referenecs, variables, and property
and method calls inside bound attributes and bound events. This is
mostly an extension of the existing indexing infrastructure.

PR Close #31535
This commit is contained in:
Ayaz Hafiz 2019-07-24 09:14:20 -07:00 committed by Miško Hevery
parent 0cd4c019cf
commit 6b67cd5620
4 changed files with 856 additions and 187 deletions

View File

@ -17,7 +17,10 @@ export enum IdentifierKind {
Property,
Method,
Element,
Template,
Attribute,
Reference,
Variable,
}
/**
@ -30,11 +33,20 @@ export interface TemplateIdentifier {
kind: IdentifierKind;
}
/** Describes a template expression, which may have a template reference or variable target. */
interface ExpressionIdentifier extends TemplateIdentifier {
/**
* ReferenceIdentifier or VariableIdentifier in the template that this identifier targets, if
* any. If the target is `null`, it points to a declaration on the component class.
* */
target: ReferenceIdentifier|VariableIdentifier|null;
}
/** Describes a property accessed in a template. */
export interface PropertyIdentifier extends TemplateIdentifier { kind: IdentifierKind.Property; }
export interface PropertyIdentifier extends ExpressionIdentifier { kind: IdentifierKind.Property; }
/** Describes a method accessed in a template. */
export interface MethodIdentifier extends TemplateIdentifier { kind: IdentifierKind.Method; }
export interface MethodIdentifier extends ExpressionIdentifier { kind: IdentifierKind.Method; }
/** Describes an element attribute in a template. */
export interface AttributeIdentifier extends TemplateIdentifier { kind: IdentifierKind.Attribute; }
@ -44,26 +56,54 @@ interface DirectiveReference {
node: ClassDeclaration;
selector: string;
}
/** A base interface for element and template identifiers. */
interface BaseElementOrTemplateIdentifier extends TemplateIdentifier {
/** Attributes on an element or template. */
attributes: Set<AttributeIdentifier>;
/** Directives applied to an element or template. */
usedDirectives: Set<DirectiveReference>;
}
/**
* Describes an indexed element in a template. The name of an `ElementIdentifier` is the entire
* element tag, which can be parsed by an indexer to determine where used directives should be
* referenced.
*/
export interface ElementIdentifier extends TemplateIdentifier {
export interface ElementIdentifier extends BaseElementOrTemplateIdentifier {
kind: IdentifierKind.Element;
/** Attributes on an element. */
attributes: Set<AttributeIdentifier>;
/** Directives applied to an element. */
usedDirectives: Set<DirectiveReference>;
}
/** Describes an indexed template node in a component template file. */
export interface TemplateNodeIdentifier extends BaseElementOrTemplateIdentifier {
kind: IdentifierKind.Template;
}
/** Describes a reference in a template like "foo" in `<div #foo></div>`. */
export interface ReferenceIdentifier extends TemplateIdentifier {
kind: IdentifierKind.Reference;
/** The target of this reference. If the target is not known, this is `null`. */
target: {
/** The template AST node that the reference targets. */
node: ElementIdentifier | TemplateIdentifier;
/**
* The directive on `node` that the reference targets. If no directive is targeted, this is
* `null`.
*/
directive: ClassDeclaration | null;
}|null;
}
/** Describes a template variable like "foo" in `<div *ngFor="let foo of foos"></div>`. */
export interface VariableIdentifier extends TemplateIdentifier { kind: IdentifierKind.Variable; }
/**
* Identifiers recorded at the top level of the template, without any context about the HTML nodes
* they were discovered in.
*/
export type TopLevelIdentifier = PropertyIdentifier | MethodIdentifier | ElementIdentifier;
export type TopLevelIdentifier = PropertyIdentifier | MethodIdentifier | ElementIdentifier |
TemplateNodeIdentifier | ReferenceIdentifier | VariableIdentifier;
/**
* Describes the absolute byte offsets of a text anchor in a source code.

View File

@ -5,8 +5,8 @@
* 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 {AST, BoundTarget, ImplicitReceiver, MethodCall, PropertyRead, RecursiveAstVisitor, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstRecursiveVisitor, TmplAstTemplate} from '@angular/compiler';
import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind, MethodIdentifier, PropertyIdentifier, TemplateIdentifier, TopLevelIdentifier} from './api';
import {AST, ASTWithSource, BoundTarget, ImplicitReceiver, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, RecursiveAstVisitor, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstRecursiveVisitor, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind, MethodIdentifier, PropertyIdentifier, ReferenceIdentifier, TemplateNodeIdentifier, TopLevelIdentifier, VariableIdentifier} from './api';
import {ComponentMeta} from './context';
/**
@ -19,6 +19,9 @@ interface HTMLNode extends TmplAstNode {
}
type ExpressionIdentifier = PropertyIdentifier | MethodIdentifier;
type TmplTarget = TmplAstReference | TmplAstVariable;
type TargetIdentifier = ReferenceIdentifier | VariableIdentifier;
type TargetIdentifierMap = Map<TmplTarget, TargetIdentifier>;
/**
* Visits the AST of an Angular template syntax expression, finding interesting
@ -33,9 +36,9 @@ class ExpressionVisitor extends RecursiveAstVisitor {
readonly identifiers: ExpressionIdentifier[] = [];
private constructor(
context: TmplAstNode, private readonly boundTemplate: BoundTarget<ComponentMeta>,
private readonly expressionStr = context.sourceSpan.toString(),
private readonly absoluteOffset = context.sourceSpan.start.offset) {
private readonly expressionStr: string, private readonly absoluteOffset: number,
private readonly boundTemplate: BoundTarget<ComponentMeta>,
private readonly targetToIdentifier: (target: TmplTarget) => TargetIdentifier) {
super();
}
@ -43,13 +46,18 @@ class ExpressionVisitor extends RecursiveAstVisitor {
* Returns identifiers discovered in an expression.
*
* @param ast expression AST to visit
* @param context HTML node expression is defined in
* @param source expression AST source code
* @param absoluteOffset absolute byte offset from start of the file to the start of the AST
* source code.
* @param boundTemplate bound target of the entire template, which can be used to query for the
* entities expressions target.
* @param targetToIdentifier closure converting a template target node to its identifier.
*/
static getIdentifiers(ast: AST, context: TmplAstNode, boundTemplate: BoundTarget<ComponentMeta>):
TopLevelIdentifier[] {
const visitor = new ExpressionVisitor(context, boundTemplate);
static getIdentifiers(
ast: AST, source: string, absoluteOffset: number, boundTemplate: BoundTarget<ComponentMeta>,
targetToIdentifier: (target: TmplTarget) => TargetIdentifier): TopLevelIdentifier[] {
const visitor =
new ExpressionVisitor(source, absoluteOffset, boundTemplate, targetToIdentifier);
visitor.visit(ast);
return visitor.identifiers;
}
@ -66,6 +74,11 @@ class ExpressionVisitor extends RecursiveAstVisitor {
super.visitPropertyRead(ast, context);
}
visitPropertyWrite(ast: PropertyWrite, context: {}) {
this.visitIdentifier(ast, IdentifierKind.Property);
super.visitPropertyWrite(ast, context);
}
/**
* Visits an identifier, adding it to the identifier store if it is useful for indexing.
*
@ -78,8 +91,7 @@ class ExpressionVisitor extends RecursiveAstVisitor {
// impossible to determine by an indexer and unsupported by the indexing module.
// The indexing module also does not currently support references to identifiers declared in the
// template itself, which have a non-null expression target.
if (!(ast.receiver instanceof ImplicitReceiver) ||
this.boundTemplate.getExpressionTarget(ast) !== null) {
if (!(ast.receiver instanceof ImplicitReceiver)) {
return;
}
@ -87,7 +99,7 @@ class ExpressionVisitor extends RecursiveAstVisitor {
// The compiler's expression parser records the location of some expressions in a manner not
// useful to the indexer. For example, a `MethodCall` `foo(a, b)` will record the span of the
// entire method call, but the indexer is interested only in the method identifier.
const localExpression = this.expressionStr.substr(ast.span.start, ast.span.end);
const localExpression = this.expressionStr.substr(ast.span.start);
if (!localExpression.includes(ast.name)) {
throw new Error(`Impossible state: "${ast.name}" not found in "${localExpression}"`);
}
@ -98,7 +110,16 @@ class ExpressionVisitor extends RecursiveAstVisitor {
const absoluteStart = this.absoluteOffset + identifierStart;
const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length);
this.identifiers.push({ name: ast.name, span, kind, } as ExpressionIdentifier);
const targetAst = this.boundTemplate.getExpressionTarget(ast);
const target = targetAst ? this.targetToIdentifier(targetAst) : null;
const identifier = {
name: ast.name,
span,
kind,
target,
} as ExpressionIdentifier;
this.identifiers.push(identifier);
}
}
@ -107,9 +128,16 @@ class ExpressionVisitor extends RecursiveAstVisitor {
* identifiers of interest, deferring to an `ExpressionVisitor` as needed.
*/
class TemplateVisitor extends TmplAstRecursiveVisitor {
// identifiers of interest found in the template
// Identifiers of interest found in the template.
readonly identifiers = new Set<TopLevelIdentifier>();
// Map of targets in a template to their identifiers.
private readonly targetIdentifierCache: TargetIdentifierMap = new Map();
// Map of elements and templates to their identifiers.
private readonly elementAndTemplateIdentifierCache =
new Map<TmplAstElement|TmplAstTemplate, ElementIdentifier|TemplateNodeIdentifier>();
/**
* Creates a template visitor for a bound template target. The bound target can be used when
* deferred to the expression visitor to get information about the target of an expression.
@ -133,29 +161,100 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
* @param element
*/
visitElement(element: TmplAstElement) {
// Record the element's attributes, which an indexer can later traverse to see if any of them
// specify a used directive on the element.
const attributes = element.attributes.map(({name, value, sourceSpan}): AttributeIdentifier => {
const elementIdentifier = this.elementOrTemplateToIdentifier(element);
this.identifiers.add(elementIdentifier);
this.visitAll(element.references);
this.visitAll(element.inputs);
this.visitAll(element.attributes);
this.visitAll(element.children);
this.visitAll(element.outputs);
}
visitTemplate(template: TmplAstTemplate) {
const templateIdentifier = this.elementOrTemplateToIdentifier(template);
this.identifiers.add(templateIdentifier);
this.visitAll(template.variables);
this.visitAll(template.attributes);
this.visitAll(template.templateAttrs);
this.visitAll(template.children);
this.visitAll(template.references);
}
visitBoundAttribute(attribute: TmplAstBoundAttribute) {
// A BoundAttribute's value (the parent AST) may have subexpressions (children ASTs) that have
// recorded spans extending past the recorded span of the parent. The most common example of
// this is with `*ngFor`.
// 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(
attribute.value, expressionSrc, expressionAbsolutePosition, this.boundTemplate,
this.targetToIdentifier);
identifiers.forEach(id => this.identifiers.add(id));
}
visitBoundEvent(attribute: TmplAstBoundEvent) { this.visitExpression(attribute.handler); }
visitBoundText(text: TmplAstBoundText) { this.visitExpression(text.value); }
visitReference(reference: TmplAstReference) {
const referenceIdentifer = this.targetToIdentifier(reference);
this.identifiers.add(referenceIdentifer);
}
visitVariable(variable: TmplAstVariable) {
const variableIdentifier = this.targetToIdentifier(variable);
this.identifiers.add(variableIdentifier);
}
/** Creates an identifier for a template element or template node. */
private elementOrTemplateToIdentifier(node: TmplAstElement|TmplAstTemplate): ElementIdentifier
|TemplateNodeIdentifier {
// If this node has already been seen, return the cached result.
if (this.elementAndTemplateIdentifierCache.has(node)) {
return this.elementAndTemplateIdentifierCache.get(node) !;
}
let name: string;
let kind: IdentifierKind.Element|IdentifierKind.Template;
if (node instanceof TmplAstTemplate) {
name = node.tagName;
kind = IdentifierKind.Template;
} else {
name = node.name;
kind = IdentifierKind.Element;
}
const {sourceSpan} = node;
// An element's or template's source span can be of the form `<element>`, `<element />`, or
// `<element></element>`. Only the selector is interesting to the indexer, so the source is
// searched for the first occurrence of the element (selector) name.
const start = this.getStartLocation(name, sourceSpan);
const absoluteSpan = new AbsoluteSourceSpan(start, start + name.length);
// Record the nodes's attributes, which an indexer can later traverse to see if any of them
// specify a used directive on the node.
const attributes = node.attributes.map(({name, sourceSpan}): AttributeIdentifier => {
return {
name,
span: new AbsoluteSourceSpan(sourceSpan.start.offset, sourceSpan.end.offset),
kind: IdentifierKind.Attribute,
};
});
const usedDirectives = this.boundTemplate.getDirectivesOfNode(element) || [];
const {name, sourceSpan} = element;
// An element's source span can be of the form `<element>`, `<element />`, or
// `<element></element>`. Only the selector is interesting to the indexer, so the source is
// searched for the first occurrence of the element (selector) name.
const localStr = sourceSpan.toString();
if (!localStr.includes(name)) {
throw new Error(`Impossible state: "${name}" not found in "${localStr}"`);
}
const start = sourceSpan.start.offset + localStr.indexOf(name);
const elId: ElementIdentifier = {
const usedDirectives = this.boundTemplate.getDirectivesOfNode(node) || [];
const identifier = {
name,
span: new AbsoluteSourceSpan(start, start + name.length),
kind: IdentifierKind.Element,
span: absoluteSpan, kind,
attributes: new Set(attributes),
usedDirectives: new Set(usedDirectives.map(dir => {
return {
@ -163,28 +262,87 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
selector: dir.selector,
};
})),
};
this.identifiers.add(elId);
// cast b/c pre-TypeScript 3.5 unions aren't well discriminated
} as ElementIdentifier |
TemplateNodeIdentifier;
this.visitAll(element.children);
this.visitAll(element.references);
this.elementAndTemplateIdentifierCache.set(node, identifier);
return identifier;
}
visitTemplate(template: TmplAstTemplate) {
this.visitAll(template.attributes);
this.visitAll(template.children);
this.visitAll(template.references);
this.visitAll(template.variables);
/** Creates an identifier for a template reference or template variable target. */
private targetToIdentifier(node: TmplAstReference|TmplAstVariable): TargetIdentifier {
// If this node has already been seen, return the cached result.
if (this.targetIdentifierCache.has(node)) {
return this.targetIdentifierCache.get(node) !;
}
const {name, sourceSpan} = node;
const start = this.getStartLocation(name, sourceSpan);
const span = new AbsoluteSourceSpan(start, start + name.length);
let identifier: ReferenceIdentifier|VariableIdentifier;
if (node instanceof TmplAstReference) {
// If the node is a reference, we care about its target. The target can be an element, a
// template, a directive applied on a template or element (in which case the directive field
// is non-null), or nothing at all.
const refTarget = this.boundTemplate.getReferenceTarget(node);
let target = null;
if (refTarget) {
if (refTarget instanceof TmplAstElement || refTarget instanceof TmplAstTemplate) {
target = {
node: this.elementOrTemplateToIdentifier(refTarget),
directive: null,
};
} else {
target = {
node: this.elementOrTemplateToIdentifier(refTarget.node),
directive: refTarget.directive.ref.node,
};
}
}
identifier = {
name,
span,
kind: IdentifierKind.Reference, target,
};
} else {
identifier = {
name,
span,
kind: IdentifierKind.Variable,
};
}
this.targetIdentifierCache.set(node, identifier);
return identifier;
}
/** Gets the start location of a string in a SourceSpan */
private getStartLocation(name: string, context: ParseSourceSpan): number {
const localStr = context.toString();
if (!localStr.includes(name)) {
throw new Error(`Impossible state: "${name}" not found in "${localStr}"`);
}
return context.start.offset + localStr.indexOf(name);
}
visitBoundText(text: TmplAstBoundText) { this.visitExpression(text); }
/**
* Visits a node's expression and adds its identifiers, if any, to the visitor's state.
* Only ASTs with information about the expression source and its location are visited.
*
* @param node node whose expression to visit
*/
private visitExpression(node: TmplAstNode&{value: AST}) {
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node, this.boundTemplate);
identifiers.forEach(id => this.identifiers.add(id));
private visitExpression(ast: AST) {
// Only include ASTs that have information about their source and absolute source spans.
if (ast instanceof ASTWithSource && ast.source !== null) {
// Make target to identifier mapping closure stateful to this visitor instance.
const targetToIdentifier = this.targetToIdentifier.bind(this);
const absoluteOffset = ast.sourceSpan.start;
const identifiers = ExpressionVisitor.getIdentifiers(
ast, ast.source, absoluteOffset, this.boundTemplate, targetToIdentifier);
identifiers.forEach(id => this.identifiers.add(id));
}
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind} from '..';
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';
@ -21,13 +21,15 @@ function bind(template: string) {
runInEachFileSystem(() => {
describe('getTemplateIdentifiers', () => {
it('should generate nothing in empty template', () => {
const refs = getTemplateIdentifiers(bind(''));
const template = '';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(0);
});
it('should ignore comments', () => {
const refs = getTemplateIdentifiers(bind('<!-- {{comment}} -->'));
const template = '<!-- {{comment}} -->';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(0);
});
@ -41,19 +43,35 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(7, 10),
target: null,
});
});
it('should ignore identifiers defined in the template', () => {
const template = `
<input #model />
{{model.valid}}
`;
it('should resist collisions', () => {
const template = '<div [bar]="bar ? bar : bar"></div>';
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
const modelId = refArr.find(ref => ref.name === 'model');
expect(modelId).toBeUndefined();
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', () => {
@ -67,6 +85,7 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
target: null,
});
});
@ -79,6 +98,7 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(13, 16),
target: null,
});
});
@ -90,6 +110,104 @@ runInEachFileSystem(() => {
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 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', () => {
@ -103,6 +221,7 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(2, 5),
target: null,
});
});
@ -115,6 +234,7 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(13, 16),
target: null,
});
});
@ -126,141 +246,489 @@ runInEachFileSystem(() => {
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
});
});
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>';
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: 'span',
kind: IdentifierKind.Element,
span: new AbsoluteSourceSpan(6, 10),
attributes: new Set(),
usedDirectives: new Set(),
name: 'bar',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(12, 15),
target: null,
});
});
it('should generate information about attributes', () => {
const template = '<div attrA attrB="val"></div>';
it('should discover method calls in template expressions', () => {
const template = '<div *ngFor="let foo of foos()"></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)',
}
]));
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)',
}
]));
});
});
});

View File

@ -70,6 +70,7 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(127, 130),
target: null,
}]),
usedComponents: new Set(),
isInline: true,
@ -97,6 +98,7 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
target: null,
}]),
usedComponents: new Set(),
isInline: false,
@ -128,6 +130,7 @@ runInEachFileSystem(() => {
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(7, 10),
target: null,
}]),
usedComponents: new Set(),
isInline: false,