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:
parent
0cd4c019cf
commit
6b67cd5620
|
@ -17,7 +17,10 @@ export enum IdentifierKind {
|
||||||
Property,
|
Property,
|
||||||
Method,
|
Method,
|
||||||
Element,
|
Element,
|
||||||
|
Template,
|
||||||
Attribute,
|
Attribute,
|
||||||
|
Reference,
|
||||||
|
Variable,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,11 +33,20 @@ export interface TemplateIdentifier {
|
||||||
kind: IdentifierKind;
|
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. */
|
/** 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. */
|
/** 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. */
|
/** Describes an element attribute in a template. */
|
||||||
export interface AttributeIdentifier extends TemplateIdentifier { kind: IdentifierKind.Attribute; }
|
export interface AttributeIdentifier extends TemplateIdentifier { kind: IdentifierKind.Attribute; }
|
||||||
|
@ -44,26 +56,54 @@ interface DirectiveReference {
|
||||||
node: ClassDeclaration;
|
node: ClassDeclaration;
|
||||||
selector: string;
|
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
|
* 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
|
* element tag, which can be parsed by an indexer to determine where used directives should be
|
||||||
* referenced.
|
* referenced.
|
||||||
*/
|
*/
|
||||||
export interface ElementIdentifier extends TemplateIdentifier {
|
export interface ElementIdentifier extends BaseElementOrTemplateIdentifier {
|
||||||
kind: IdentifierKind.Element;
|
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
|
* Identifiers recorded at the top level of the template, without any context about the HTML nodes
|
||||||
* they were discovered in.
|
* 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.
|
* Describes the absolute byte offsets of a text anchor in a source code.
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {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, TemplateIdentifier, TopLevelIdentifier} from './api';
|
import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind, MethodIdentifier, PropertyIdentifier, ReferenceIdentifier, TemplateNodeIdentifier, TopLevelIdentifier, VariableIdentifier} from './api';
|
||||||
import {ComponentMeta} from './context';
|
import {ComponentMeta} from './context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,6 +19,9 @@ interface HTMLNode extends TmplAstNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpressionIdentifier = PropertyIdentifier | MethodIdentifier;
|
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
|
* Visits the AST of an Angular template syntax expression, finding interesting
|
||||||
|
@ -33,9 +36,9 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
||||||
readonly identifiers: ExpressionIdentifier[] = [];
|
readonly identifiers: ExpressionIdentifier[] = [];
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
context: TmplAstNode, private readonly boundTemplate: BoundTarget<ComponentMeta>,
|
private readonly expressionStr: string, private readonly absoluteOffset: number,
|
||||||
private readonly expressionStr = context.sourceSpan.toString(),
|
private readonly boundTemplate: BoundTarget<ComponentMeta>,
|
||||||
private readonly absoluteOffset = context.sourceSpan.start.offset) {
|
private readonly targetToIdentifier: (target: TmplTarget) => TargetIdentifier) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,13 +46,18 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
||||||
* Returns identifiers discovered in an expression.
|
* Returns identifiers discovered in an expression.
|
||||||
*
|
*
|
||||||
* @param ast expression AST to visit
|
* @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
|
* @param boundTemplate bound target of the entire template, which can be used to query for the
|
||||||
* entities expressions target.
|
* entities expressions target.
|
||||||
|
* @param targetToIdentifier closure converting a template target node to its identifier.
|
||||||
*/
|
*/
|
||||||
static getIdentifiers(ast: AST, context: TmplAstNode, boundTemplate: BoundTarget<ComponentMeta>):
|
static getIdentifiers(
|
||||||
TopLevelIdentifier[] {
|
ast: AST, source: string, absoluteOffset: number, boundTemplate: BoundTarget<ComponentMeta>,
|
||||||
const visitor = new ExpressionVisitor(context, boundTemplate);
|
targetToIdentifier: (target: TmplTarget) => TargetIdentifier): TopLevelIdentifier[] {
|
||||||
|
const visitor =
|
||||||
|
new ExpressionVisitor(source, absoluteOffset, boundTemplate, targetToIdentifier);
|
||||||
visitor.visit(ast);
|
visitor.visit(ast);
|
||||||
return visitor.identifiers;
|
return visitor.identifiers;
|
||||||
}
|
}
|
||||||
|
@ -66,6 +74,11 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
||||||
super.visitPropertyRead(ast, context);
|
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.
|
* 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.
|
// 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
|
// The indexing module also does not currently support references to identifiers declared in the
|
||||||
// template itself, which have a non-null expression target.
|
// template itself, which have a non-null expression target.
|
||||||
if (!(ast.receiver instanceof ImplicitReceiver) ||
|
if (!(ast.receiver instanceof ImplicitReceiver)) {
|
||||||
this.boundTemplate.getExpressionTarget(ast) !== null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +99,7 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
||||||
// The compiler's expression parser records the location of some expressions in a manner not
|
// 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
|
// 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.
|
// 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)) {
|
if (!localExpression.includes(ast.name)) {
|
||||||
throw new Error(`Impossible state: "${ast.name}" not found in "${localExpression}"`);
|
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 absoluteStart = this.absoluteOffset + identifierStart;
|
||||||
const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length);
|
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.
|
* identifiers of interest, deferring to an `ExpressionVisitor` as needed.
|
||||||
*/
|
*/
|
||||||
class TemplateVisitor extends TmplAstRecursiveVisitor {
|
class TemplateVisitor extends TmplAstRecursiveVisitor {
|
||||||
// identifiers of interest found in the template
|
// Identifiers of interest found in the template.
|
||||||
readonly identifiers = new Set<TopLevelIdentifier>();
|
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
|
* 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.
|
* deferred to the expression visitor to get information about the target of an expression.
|
||||||
|
@ -133,29 +161,100 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
|
||||||
* @param element
|
* @param element
|
||||||
*/
|
*/
|
||||||
visitElement(element: TmplAstElement) {
|
visitElement(element: TmplAstElement) {
|
||||||
// Record the element's attributes, which an indexer can later traverse to see if any of them
|
const elementIdentifier = this.elementOrTemplateToIdentifier(element);
|
||||||
// specify a used directive on the element.
|
|
||||||
const attributes = element.attributes.map(({name, value, sourceSpan}): AttributeIdentifier => {
|
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 {
|
return {
|
||||||
name,
|
name,
|
||||||
span: new AbsoluteSourceSpan(sourceSpan.start.offset, sourceSpan.end.offset),
|
span: new AbsoluteSourceSpan(sourceSpan.start.offset, sourceSpan.end.offset),
|
||||||
kind: IdentifierKind.Attribute,
|
kind: IdentifierKind.Attribute,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const usedDirectives = this.boundTemplate.getDirectivesOfNode(element) || [];
|
const usedDirectives = this.boundTemplate.getDirectivesOfNode(node) || [];
|
||||||
const {name, sourceSpan} = element;
|
|
||||||
// An element's source span can be of the form `<element>`, `<element />`, or
|
const identifier = {
|
||||||
// `<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 = {
|
|
||||||
name,
|
name,
|
||||||
span: new AbsoluteSourceSpan(start, start + name.length),
|
span: absoluteSpan, kind,
|
||||||
kind: IdentifierKind.Element,
|
|
||||||
attributes: new Set(attributes),
|
attributes: new Set(attributes),
|
||||||
usedDirectives: new Set(usedDirectives.map(dir => {
|
usedDirectives: new Set(usedDirectives.map(dir => {
|
||||||
return {
|
return {
|
||||||
|
@ -163,28 +262,87 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
|
||||||
selector: dir.selector,
|
selector: dir.selector,
|
||||||
};
|
};
|
||||||
})),
|
})),
|
||||||
};
|
// cast b/c pre-TypeScript 3.5 unions aren't well discriminated
|
||||||
this.identifiers.add(elId);
|
} as ElementIdentifier |
|
||||||
|
TemplateNodeIdentifier;
|
||||||
|
|
||||||
this.visitAll(element.children);
|
this.elementAndTemplateIdentifierCache.set(node, identifier);
|
||||||
this.visitAll(element.references);
|
return identifier;
|
||||||
}
|
}
|
||||||
visitTemplate(template: TmplAstTemplate) {
|
|
||||||
this.visitAll(template.attributes);
|
/** Creates an identifier for a template reference or template variable target. */
|
||||||
this.visitAll(template.children);
|
private targetToIdentifier(node: TmplAstReference|TmplAstVariable): TargetIdentifier {
|
||||||
this.visitAll(template.references);
|
// If this node has already been seen, return the cached result.
|
||||||
this.visitAll(template.variables);
|
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.
|
* 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
|
* @param node node whose expression to visit
|
||||||
*/
|
*/
|
||||||
private visitExpression(node: TmplAstNode&{value: AST}) {
|
private visitExpression(ast: AST) {
|
||||||
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node, this.boundTemplate);
|
// Only include ASTs that have information about their source and absolute source spans.
|
||||||
identifiers.forEach(id => this.identifiers.add(id));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {runInEachFileSystem} from '../../file_system/testing';
|
||||||
import {getTemplateIdentifiers} from '../src/template';
|
import {getTemplateIdentifiers} from '../src/template';
|
||||||
import * as util from './util';
|
import * as util from './util';
|
||||||
|
@ -21,13 +21,15 @@ function bind(template: string) {
|
||||||
runInEachFileSystem(() => {
|
runInEachFileSystem(() => {
|
||||||
describe('getTemplateIdentifiers', () => {
|
describe('getTemplateIdentifiers', () => {
|
||||||
it('should generate nothing in empty template', () => {
|
it('should generate nothing in empty template', () => {
|
||||||
const refs = getTemplateIdentifiers(bind(''));
|
const template = '';
|
||||||
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
expect(refs.size).toBe(0);
|
expect(refs.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore comments', () => {
|
it('should ignore comments', () => {
|
||||||
const refs = getTemplateIdentifiers(bind('<!-- {{comment}} -->'));
|
const template = '<!-- {{comment}} -->';
|
||||||
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
expect(refs.size).toBe(0);
|
expect(refs.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
@ -41,19 +43,35 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(7, 10),
|
span: new AbsoluteSourceSpan(7, 10),
|
||||||
|
target: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore identifiers defined in the template', () => {
|
it('should resist collisions', () => {
|
||||||
const template = `
|
const template = '<div [bar]="bar ? bar : bar"></div>';
|
||||||
<input #model />
|
|
||||||
{{model.valid}}
|
|
||||||
`;
|
|
||||||
const refs = getTemplateIdentifiers(bind(template));
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
const refArr = Array.from(refs);
|
const refArr = Array.from(refs);
|
||||||
const modelId = refArr.find(ref => ref.name === 'model');
|
expect(refArr).toEqual(jasmine.arrayContaining([
|
||||||
expect(modelId).toBeUndefined();
|
{
|
||||||
|
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', () => {
|
describe('generates identifiers for PropertyReads', () => {
|
||||||
|
@ -67,6 +85,7 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(2, 5),
|
span: new AbsoluteSourceSpan(2, 5),
|
||||||
|
target: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -79,6 +98,7 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(13, 16),
|
span: new AbsoluteSourceSpan(13, 16),
|
||||||
|
target: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,6 +110,104 @@ runInEachFileSystem(() => {
|
||||||
const [ref] = Array.from(refs);
|
const [ref] = Array.from(refs);
|
||||||
expect(ref.name).toBe('foo');
|
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', () => {
|
describe('generates identifiers for MethodCalls', () => {
|
||||||
|
@ -103,6 +221,7 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Method,
|
kind: IdentifierKind.Method,
|
||||||
span: new AbsoluteSourceSpan(2, 5),
|
span: new AbsoluteSourceSpan(2, 5),
|
||||||
|
target: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -115,6 +234,7 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Method,
|
kind: IdentifierKind.Method,
|
||||||
span: new AbsoluteSourceSpan(13, 16),
|
span: new AbsoluteSourceSpan(13, 16),
|
||||||
|
target: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -126,141 +246,489 @@ runInEachFileSystem(() => {
|
||||||
const [ref] = Array.from(refs);
|
const [ref] = Array.from(refs);
|
||||||
expect(ref.name).toBe('foo');
|
expect(ref.name).toBe('foo');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('generates identifiers for elements', () => {
|
it('should discover method calls in bound attributes', () => {
|
||||||
it('should record elements as ElementIdentifiers', () => {
|
const template = '<div [bar]="bar()"></div>';
|
||||||
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 refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
const refArr = Array.from(refs);
|
const refArr = Array.from(refs);
|
||||||
expect(refArr).toContain({
|
expect(refArr).toContain({
|
||||||
name: 'span',
|
name: 'bar',
|
||||||
kind: IdentifierKind.Element,
|
kind: IdentifierKind.Method,
|
||||||
span: new AbsoluteSourceSpan(6, 10),
|
span: new AbsoluteSourceSpan(12, 15),
|
||||||
attributes: new Set(),
|
target: null,
|
||||||
usedDirectives: new Set(),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate information about attributes', () => {
|
it('should discover method calls in template expressions', () => {
|
||||||
const template = '<div attrA attrB="val"></div>';
|
const template = '<div *ngFor="let foo of foos()"></div>';
|
||||||
const refs = getTemplateIdentifiers(bind(template));
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
const [ref] = Array.from(refs);
|
const refArr = Array.from(refs);
|
||||||
const attrs = (ref as ElementIdentifier).attributes;
|
expect(refArr).toContain({
|
||||||
expect(attrs).toEqual(new Set<AttributeIdentifier>([
|
name: 'foos',
|
||||||
{
|
kind: IdentifierKind.Method,
|
||||||
name: 'attrA',
|
span: new AbsoluteSourceSpan(24, 28),
|
||||||
kind: IdentifierKind.Attribute,
|
target: null,
|
||||||
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 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)',
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -70,6 +70,7 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(127, 130),
|
span: new AbsoluteSourceSpan(127, 130),
|
||||||
|
target: null,
|
||||||
}]),
|
}]),
|
||||||
usedComponents: new Set(),
|
usedComponents: new Set(),
|
||||||
isInline: true,
|
isInline: true,
|
||||||
|
@ -97,6 +98,7 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(2, 5),
|
span: new AbsoluteSourceSpan(2, 5),
|
||||||
|
target: null,
|
||||||
}]),
|
}]),
|
||||||
usedComponents: new Set(),
|
usedComponents: new Set(),
|
||||||
isInline: false,
|
isInline: false,
|
||||||
|
@ -128,6 +130,7 @@ runInEachFileSystem(() => {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(7, 10),
|
span: new AbsoluteSourceSpan(7, 10),
|
||||||
|
target: null,
|
||||||
}]),
|
}]),
|
||||||
usedComponents: new Set(),
|
usedComponents: new Set(),
|
||||||
isInline: false,
|
isInline: false,
|
||||||
|
|
Loading…
Reference in New Issue