diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts index 978d635719..2f6095d499 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts @@ -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; + + /** Directives applied to an element or template. */ + usedDirectives: Set; +} /** * 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; - - /** Directives applied to an element. */ - usedDirectives: Set; } +/** 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 `
`. */ +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 `
`. */ +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. diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts index 3b68d15318..aeb2551240 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts @@ -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; /** * 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, - 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, + 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): - TopLevelIdentifier[] { - const visitor = new ExpressionVisitor(context, boundTemplate); + static getIdentifiers( + ast: AST, source: string, absoluteOffset: number, boundTemplate: BoundTarget, + 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(); + // 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(); + /** * 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 ``, ``, or + // ``. 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 ``, ``, or - // ``. 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)); + } } } diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts index 7f27a7b340..e02f5e96b2 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts @@ -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('')); + const template = ''; + 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 = ` - - {{model.valid}} - `; + it('should resist collisions', () => { + const template = '
'; 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 = '
'; + 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 = '
'; + 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 = '
'; + 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 = '
'; + 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 = '
'; + 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 = '
'; + 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 = ''; - 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 = ''; - 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 = ''; - 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 = ''; - 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 = ' text '; - 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 = '
'; + it('should discover method calls in bound attributes', () => { + const template = '
'; 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 = '
'; + it('should discover method calls in template expressions', () => { + const template = '
'; const refs = getTemplateIdentifiers(bind(template)); - const [ref] = Array.from(refs); - const attrs = (ref as ElementIdentifier).attributes; - expect(attrs).toEqual(new Set([ - { - 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 = ''; - 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 = '
'; + 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 = '
'; + 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 = `
{{foo.className}}
`; + 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}}
`; + 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 = '
'; + 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 = `
`; + 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 = '
'; + 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 = ''; + 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 = '
'; + 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 = `
{{foo + i}}
`; + 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 = `
`; + 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 = ''; + 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 = ''; + 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 = ''; + 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 = ''; + 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 = ' text '; + 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 = '
'; + 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 = '
'; + const refs = getTemplateIdentifiers(bind(template)); + + const [ref] = Array.from(refs); + const attrs = (ref as ElementIdentifier).attributes; + expect(attrs).toEqual(new Set([ + { + 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 = ''; + 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 = ''; + 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 = ''; + 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 = '
'; + 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 = ''; + const refs = getTemplateIdentifiers(bind(template)); + + const [ref] = Array.from(refs); + const attrs = (ref as TemplateNodeIdentifier).attributes; + expect(attrs).toEqual(new Set([ + { + 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 = ''; + 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)', + } + ])); + }); + }); }); diff --git a/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts index 505e64c57a..ff05d00593 100644 --- a/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts +++ b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts @@ -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,