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,
|
||||
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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)',
|
||||
}
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue