feat(ivy): index template elements for selectors, attributes, directives (#31240)
Add support for indexing elements in the indexing module. Opening and self-closing HTML tags have their selector indexed, as well as the attributes on the element and the directives applied to an element. PR Close #31240
This commit is contained in:
parent
c4c340a7c4
commit
604d9063c5
@ -15,13 +15,8 @@ import * as ts from 'typescript';
|
|||||||
export enum IdentifierKind {
|
export enum IdentifierKind {
|
||||||
Property,
|
Property,
|
||||||
Method,
|
Method,
|
||||||
}
|
Element,
|
||||||
|
Attribute,
|
||||||
/**
|
|
||||||
* Describes the absolute byte offsets of a text anchor in a source code.
|
|
||||||
*/
|
|
||||||
export class AbsoluteSourceSpan {
|
|
||||||
constructor(public start: number, public end: number) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,6 +29,43 @@ export interface TemplateIdentifier {
|
|||||||
kind: IdentifierKind;
|
kind: IdentifierKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Describes a property accessed in a template. */
|
||||||
|
export interface PropertyIdentifier extends TemplateIdentifier { kind: IdentifierKind.Property; }
|
||||||
|
|
||||||
|
/** Describes a method accessed in a template. */
|
||||||
|
export interface MethodIdentifier extends TemplateIdentifier { kind: IdentifierKind.Method; }
|
||||||
|
|
||||||
|
/** Describes an element attribute in a template. */
|
||||||
|
export interface AttributeIdentifier extends TemplateIdentifier { kind: IdentifierKind.Attribute; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
kind: IdentifierKind.Element;
|
||||||
|
|
||||||
|
/** Attributes on an element. */
|
||||||
|
attributes: Set<AttributeIdentifier>;
|
||||||
|
|
||||||
|
/** Directives applied to an element. */
|
||||||
|
usedDirectives: Set<ts.Declaration>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the absolute byte offsets of a text anchor in a source code.
|
||||||
|
*/
|
||||||
|
export class AbsoluteSourceSpan {
|
||||||
|
constructor(public start: number, public end: number) {}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes an analyzed, indexed component and its template.
|
* Describes an analyzed, indexed component and its template.
|
||||||
*/
|
*/
|
||||||
@ -42,7 +74,7 @@ export interface IndexedComponent {
|
|||||||
selector: string|null;
|
selector: string|null;
|
||||||
file: ParseSourceFile;
|
file: ParseSourceFile;
|
||||||
template: {
|
template: {
|
||||||
identifiers: Set<TemplateIdentifier>,
|
identifiers: Set<TopLevelIdentifier>,
|
||||||
usedComponents: Set<ts.Declaration>,
|
usedComponents: Set<ts.Declaration>,
|
||||||
isInline: boolean,
|
isInline: boolean,
|
||||||
file: ParseSourceFile;
|
file: ParseSourceFile;
|
||||||
|
@ -6,9 +6,10 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, BoundTarget, DirectiveMeta, ImplicitReceiver, MethodCall, PropertyRead, RecursiveAstVisitor} from '@angular/compiler';
|
import {AST, BoundTarget, ImplicitReceiver, MethodCall, PropertyRead, RecursiveAstVisitor} from '@angular/compiler';
|
||||||
import {BoundText, Element, Node, RecursiveVisitor as RecursiveTemplateVisitor, Template} from '@angular/compiler/src/render3/r3_ast';
|
import {BoundText, Element, Node, RecursiveVisitor as RecursiveTemplateVisitor, Template} from '@angular/compiler/src/render3/r3_ast';
|
||||||
import {AbsoluteSourceSpan, IdentifierKind, TemplateIdentifier} from './api';
|
import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind, MethodIdentifier, PropertyIdentifier, TemplateIdentifier, TopLevelIdentifier} from './api';
|
||||||
|
import {ComponentMeta} from './context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A parsed node in a template, which may have a name (if it is a selector) or
|
* A parsed node in a template, which may have a name (if it is a selector) or
|
||||||
@ -19,20 +20,22 @@ interface HTMLNode extends Node {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExpressionIdentifier = PropertyIdentifier | MethodIdentifier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visits the AST of an Angular template syntax expression, finding interesting
|
* Visits the AST of an Angular template syntax expression, finding interesting
|
||||||
* entities (variable references, etc.). Creates an array of Entities found in
|
* entities (variable references, etc.). Creates an array of Entities found in
|
||||||
* the expression, with the location of the Entities being relative to the
|
* the expression, with the location of the Entities being relative to the
|
||||||
* expression.
|
* expression.
|
||||||
*
|
*
|
||||||
* Visiting `text {{prop}}` will return `[TemplateIdentifier {name: 'prop', span: {start: 7, end:
|
* Visiting `text {{prop}}` will return
|
||||||
* 11}}]`.
|
* `[TopLevelIdentifier {name: 'prop', span: {start: 7, end: 11}}]`.
|
||||||
*/
|
*/
|
||||||
class ExpressionVisitor extends RecursiveAstVisitor {
|
class ExpressionVisitor extends RecursiveAstVisitor {
|
||||||
readonly identifiers: TemplateIdentifier[] = [];
|
readonly identifiers: ExpressionIdentifier[] = [];
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
context: Node, private readonly boundTemplate: BoundTarget<DirectiveMeta>,
|
context: Node, private readonly boundTemplate: BoundTarget<ComponentMeta>,
|
||||||
private readonly expressionStr = context.sourceSpan.toString(),
|
private readonly expressionStr = context.sourceSpan.toString(),
|
||||||
private readonly absoluteOffset = context.sourceSpan.start.offset) {
|
private readonly absoluteOffset = context.sourceSpan.start.offset) {
|
||||||
super();
|
super();
|
||||||
@ -46,8 +49,8 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
|||||||
* @param boundTemplate bound target of the entire template, which can be used to query for the
|
* @param boundTemplate bound target of the entire template, which can be used to query for the
|
||||||
* entities expressions target.
|
* entities expressions target.
|
||||||
*/
|
*/
|
||||||
static getIdentifiers(ast: AST, context: Node, boundTemplate: BoundTarget<DirectiveMeta>):
|
static getIdentifiers(ast: AST, context: Node, boundTemplate: BoundTarget<ComponentMeta>):
|
||||||
TemplateIdentifier[] {
|
TopLevelIdentifier[] {
|
||||||
const visitor = new ExpressionVisitor(context, boundTemplate);
|
const visitor = new ExpressionVisitor(context, boundTemplate);
|
||||||
visitor.visit(ast);
|
visitor.visit(ast);
|
||||||
return visitor.identifiers;
|
return visitor.identifiers;
|
||||||
@ -71,7 +74,8 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
|||||||
* @param ast expression AST the identifier is in
|
* @param ast expression AST the identifier is in
|
||||||
* @param kind identifier kind
|
* @param kind identifier kind
|
||||||
*/
|
*/
|
||||||
private visitIdentifier(ast: AST&{name: string, receiver: AST}, kind: IdentifierKind) {
|
private visitIdentifier(
|
||||||
|
ast: AST&{name: string, receiver: AST}, kind: ExpressionIdentifier['kind']) {
|
||||||
// The definition of a non-top-level property such as `bar` in `{{foo.bar}}` is currently
|
// The definition of a non-top-level property such as `bar` in `{{foo.bar}}` is currently
|
||||||
// impossible to determine by an indexer and unsupported by the indexing module.
|
// impossible to determine by an indexer and unsupported by the indexing module.
|
||||||
// The indexing module also does not currently support references to identifiers declared in the
|
// The indexing module also does not currently support references to identifiers declared in the
|
||||||
@ -86,6 +90,9 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
|||||||
// useful to the indexer. For example, a `MethodCall` `foo(a, b)` will record the span of the
|
// useful to the indexer. For example, a `MethodCall` `foo(a, b)` will record the span of the
|
||||||
// entire method call, but the indexer is interested only in the method identifier.
|
// entire method call, but the indexer is interested only in the method identifier.
|
||||||
const localExpression = this.expressionStr.substr(ast.span.start, ast.span.end);
|
const localExpression = this.expressionStr.substr(ast.span.start, ast.span.end);
|
||||||
|
if (!localExpression.includes(ast.name)) {
|
||||||
|
throw new Error(`Impossible state: "${ast.name}" not found in "${localExpression}"`);
|
||||||
|
}
|
||||||
const identifierStart = ast.span.start + localExpression.indexOf(ast.name);
|
const identifierStart = ast.span.start + localExpression.indexOf(ast.name);
|
||||||
|
|
||||||
// Join the relative position of the expression within a node with the absolute position
|
// Join the relative position of the expression within a node with the absolute position
|
||||||
@ -93,11 +100,7 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
|||||||
const absoluteStart = this.absoluteOffset + identifierStart;
|
const absoluteStart = this.absoluteOffset + identifierStart;
|
||||||
const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length);
|
const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length);
|
||||||
|
|
||||||
this.identifiers.push({
|
this.identifiers.push({ name: ast.name, span, kind, } as ExpressionIdentifier);
|
||||||
name: ast.name,
|
|
||||||
span,
|
|
||||||
kind,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +110,7 @@ class ExpressionVisitor extends RecursiveAstVisitor {
|
|||||||
*/
|
*/
|
||||||
class TemplateVisitor extends RecursiveTemplateVisitor {
|
class TemplateVisitor extends RecursiveTemplateVisitor {
|
||||||
// identifiers of interest found in the template
|
// identifiers of interest found in the template
|
||||||
readonly identifiers = new Set<TemplateIdentifier>();
|
readonly identifiers = new Set<TopLevelIdentifier>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a template visitor for a bound template target. The bound target can be used when
|
* Creates a template visitor for a bound template target. The bound target can be used when
|
||||||
@ -115,7 +118,7 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
|
|||||||
*
|
*
|
||||||
* @param boundTemplate bound template target
|
* @param boundTemplate bound template target
|
||||||
*/
|
*/
|
||||||
constructor(private boundTemplate: BoundTarget<DirectiveMeta>) { super(); }
|
constructor(private boundTemplate: BoundTarget<ComponentMeta>) { super(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visits a node in the template.
|
* Visits a node in the template.
|
||||||
@ -126,8 +129,40 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
|
|||||||
|
|
||||||
visitAll(nodes: Node[]) { nodes.forEach(node => this.visit(node)); }
|
visitAll(nodes: Node[]) { nodes.forEach(node => this.visit(node)); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an identifier for an HTML element and visit its children recursively.
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
*/
|
||||||
visitElement(element: Element) {
|
visitElement(element: Element) {
|
||||||
this.visitAll(element.attributes);
|
// 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 => {
|
||||||
|
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 = {
|
||||||
|
name,
|
||||||
|
span: new AbsoluteSourceSpan(start, start + name.length),
|
||||||
|
kind: IdentifierKind.Element,
|
||||||
|
attributes: new Set(attributes),
|
||||||
|
usedDirectives: new Set(usedDirectives.map(dir => dir.ref.node)),
|
||||||
|
};
|
||||||
|
this.identifiers.add(elId);
|
||||||
|
|
||||||
this.visitAll(element.children);
|
this.visitAll(element.children);
|
||||||
this.visitAll(element.references);
|
this.visitAll(element.references);
|
||||||
}
|
}
|
||||||
@ -142,7 +177,7 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
|
|||||||
/**
|
/**
|
||||||
* Visits a node's expression and adds its identifiers, if any, to the visitor's state.
|
* Visits a node's expression and adds its identifiers, if any, to the visitor's state.
|
||||||
*
|
*
|
||||||
* @param curretNode node whose expression to visit
|
* @param node node whose expression to visit
|
||||||
*/
|
*/
|
||||||
private visitExpression(node: Node&{value: AST}) {
|
private visitExpression(node: Node&{value: AST}) {
|
||||||
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node, this.boundTemplate);
|
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node, this.boundTemplate);
|
||||||
@ -156,8 +191,8 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
|
|||||||
* @param boundTemplate bound template target, which can be used for querying expression targets.
|
* @param boundTemplate bound template target, which can be used for querying expression targets.
|
||||||
* @return identifiers in template
|
* @return identifiers in template
|
||||||
*/
|
*/
|
||||||
export function getTemplateIdentifiers(boundTemplate: BoundTarget<DirectiveMeta>):
|
export function getTemplateIdentifiers(boundTemplate: BoundTarget<ComponentMeta>):
|
||||||
Set<TemplateIdentifier> {
|
Set<TopLevelIdentifier> {
|
||||||
const visitor = new TemplateVisitor(boundTemplate);
|
const visitor = new TemplateVisitor(boundTemplate);
|
||||||
if (boundTemplate.target.template !== undefined) {
|
if (boundTemplate.target.template !== undefined) {
|
||||||
visitor.visitAll(boundTemplate.target.template);
|
visitor.visitAll(boundTemplate.target.template);
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AbsoluteSourceSpan, IdentifierKind} from '..';
|
import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind} from '..';
|
||||||
import {runInEachFileSystem} from '../../file_system/testing';
|
import {runInEachFileSystem} from '../../file_system/testing';
|
||||||
import {getTemplateIdentifiers} from '../src/template';
|
import {getTemplateIdentifiers} from '../src/template';
|
||||||
import * as util from './util';
|
import * as util from './util';
|
||||||
@ -20,8 +20,8 @@ function bind(template: string) {
|
|||||||
|
|
||||||
runInEachFileSystem(() => {
|
runInEachFileSystem(() => {
|
||||||
describe('getTemplateIdentifiers', () => {
|
describe('getTemplateIdentifiers', () => {
|
||||||
it('should generate nothing in HTML-only template', () => {
|
it('should generate nothing in empty template', () => {
|
||||||
const refs = getTemplateIdentifiers(bind('<div></div>'));
|
const refs = getTemplateIdentifiers(bind(''));
|
||||||
|
|
||||||
expect(refs.size).toBe(0);
|
expect(refs.size).toBe(0);
|
||||||
});
|
});
|
||||||
@ -33,14 +33,14 @@ runInEachFileSystem(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle arbitrary whitespace', () => {
|
it('should handle arbitrary whitespace', () => {
|
||||||
const template = '<div>\n\n {{foo}}</div>';
|
const template = '\n\n {{foo}}';
|
||||||
const refs = getTemplateIdentifiers(bind(template));
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
const [ref] = Array.from(refs);
|
const [ref] = Array.from(refs);
|
||||||
expect(ref).toEqual({
|
expect(ref).toEqual({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(12, 15),
|
span: new AbsoluteSourceSpan(7, 10),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -75,11 +75,11 @@ runInEachFileSystem(() => {
|
|||||||
const refs = getTemplateIdentifiers(bind(template));
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
const refArr = Array.from(refs);
|
const refArr = Array.from(refs);
|
||||||
expect(refArr).toEqual(jasmine.arrayContaining([{
|
expect(refArr).toContain({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(13, 16),
|
span: new AbsoluteSourceSpan(13, 16),
|
||||||
}]));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore identifiers that are not implicitly received by the template', () => {
|
it('should ignore identifiers that are not implicitly received by the template', () => {
|
||||||
@ -111,11 +111,11 @@ runInEachFileSystem(() => {
|
|||||||
const refs = getTemplateIdentifiers(bind(template));
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
const refArr = Array.from(refs);
|
const refArr = Array.from(refs);
|
||||||
expect(refArr).toEqual(jasmine.arrayContaining([{
|
expect(refArr).toContain({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Method,
|
kind: IdentifierKind.Method,
|
||||||
span: new AbsoluteSourceSpan(13, 16),
|
span: new AbsoluteSourceSpan(13, 16),
|
||||||
}]));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore identifiers that are not implicitly received by the template', () => {
|
it('should ignore identifiers that are not implicitly received by the template', () => {
|
||||||
@ -127,5 +127,127 @@ runInEachFileSystem(() => {
|
|||||||
expect(ref.name).toBe('foo');
|
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>';
|
||||||
|
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([declA, declB, declC]));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,9 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
import {BoundTarget, ParseSourceFile} from '@angular/compiler';
|
import {BoundTarget, ParseSourceFile} from '@angular/compiler';
|
||||||
import {runInEachFileSystem} from '../../file_system/testing';
|
import {runInEachFileSystem} from '../../file_system/testing';
|
||||||
import {DirectiveMeta} from '../../metadata';
|
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {IndexingContext} from '../src/context';
|
import {ComponentMeta, IndexingContext} from '../src/context';
|
||||||
import {getTemplateIdentifiers} from '../src/template';
|
import {getTemplateIdentifiers} from '../src/template';
|
||||||
import {generateAnalysis} from '../src/transform';
|
import {generateAnalysis} from '../src/transform';
|
||||||
import * as util from './util';
|
import * as util from './util';
|
||||||
@ -19,7 +18,7 @@ import * as util from './util';
|
|||||||
*/
|
*/
|
||||||
function populateContext(
|
function populateContext(
|
||||||
context: IndexingContext, component: ClassDeclaration, selector: string, template: string,
|
context: IndexingContext, component: ClassDeclaration, selector: string, template: string,
|
||||||
boundTemplate: BoundTarget<DirectiveMeta>, isInline: boolean = false) {
|
boundTemplate: BoundTarget<ComponentMeta>, isInline: boolean = false) {
|
||||||
context.addComponent({
|
context.addComponent({
|
||||||
declaration: component,
|
declaration: component,
|
||||||
selector,
|
selector,
|
||||||
|
@ -10,9 +10,9 @@ import {BoundTarget, CssSelector, ParseTemplateOptions, R3TargetBinder, Selector
|
|||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
|
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
|
||||||
import {Reference} from '../../imports';
|
import {Reference} from '../../imports';
|
||||||
import {DirectiveMeta} from '../../metadata';
|
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {getDeclaration, makeProgram} from '../../testing';
|
import {getDeclaration, makeProgram} from '../../testing';
|
||||||
|
import {ComponentMeta} from '../src/context';
|
||||||
|
|
||||||
/** Dummy file URL */
|
/** Dummy file URL */
|
||||||
export function getTestFilePath(): AbsoluteFsPath {
|
export function getTestFilePath(): AbsoluteFsPath {
|
||||||
@ -41,16 +41,12 @@ export function getComponentDeclaration(componentStr: string, className: string)
|
|||||||
export function getBoundTemplate(
|
export function getBoundTemplate(
|
||||||
template: string, options: ParseTemplateOptions = {},
|
template: string, options: ParseTemplateOptions = {},
|
||||||
components: Array<{selector: string, declaration: ClassDeclaration}> =
|
components: Array<{selector: string, declaration: ClassDeclaration}> =
|
||||||
[]): BoundTarget<DirectiveMeta> {
|
[]): BoundTarget<ComponentMeta> {
|
||||||
const matcher = new SelectorMatcher<DirectiveMeta>();
|
const matcher = new SelectorMatcher<ComponentMeta>();
|
||||||
components.forEach(({selector, declaration}) => {
|
components.forEach(({selector, declaration}) => {
|
||||||
matcher.addSelectables(CssSelector.parse(selector), {
|
matcher.addSelectables(CssSelector.parse(selector), {
|
||||||
ref: new Reference(declaration),
|
ref: new Reference(declaration),
|
||||||
selector,
|
selector,
|
||||||
queries: [],
|
|
||||||
ngTemplateGuards: [],
|
|
||||||
hasNgTemplateContextGuard: false,
|
|
||||||
baseClass: null,
|
|
||||||
name: declaration.name.getText(),
|
name: declaration.name.getText(),
|
||||||
isComponent: true,
|
isComponent: true,
|
||||||
inputs: {},
|
inputs: {},
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import {AbsoluteFsPath, resolve} from '@angular/compiler-cli/src/ngtsc/file_system';
|
import {AbsoluteFsPath, resolve} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||||
import {AbsoluteSourceSpan, IdentifierKind} from '@angular/compiler-cli/src/ngtsc/indexer';
|
import {AbsoluteSourceSpan, IdentifierKind, TopLevelIdentifier} from '@angular/compiler-cli/src/ngtsc/indexer';
|
||||||
import {ParseSourceFile} from '@angular/compiler/src/compiler';
|
import {ParseSourceFile} from '@angular/compiler/src/compiler';
|
||||||
|
|
||||||
import {NgtscTestEnvironment} from './env';
|
import {NgtscTestEnvironment} from './env';
|
||||||
@ -66,7 +66,7 @@ runInEachFileSystem(() => {
|
|||||||
const template = indexedComp.template;
|
const template = indexedComp.template;
|
||||||
|
|
||||||
expect(template).toEqual({
|
expect(template).toEqual({
|
||||||
identifiers: new Set([{
|
identifiers: new Set<TopLevelIdentifier>([{
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(127, 130),
|
span: new AbsoluteSourceSpan(127, 130),
|
||||||
@ -93,7 +93,7 @@ runInEachFileSystem(() => {
|
|||||||
const template = indexedComp.template;
|
const template = indexedComp.template;
|
||||||
|
|
||||||
expect(template).toEqual({
|
expect(template).toEqual({
|
||||||
identifiers: new Set([{
|
identifiers: new Set<TopLevelIdentifier>([{
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(2, 5),
|
span: new AbsoluteSourceSpan(2, 5),
|
||||||
@ -118,20 +118,20 @@ runInEachFileSystem(() => {
|
|||||||
})
|
})
|
||||||
export class TestCmp { foo = 0; }
|
export class TestCmp { foo = 0; }
|
||||||
`);
|
`);
|
||||||
env.write(testTemplateFile, '<div> \n {{foo}}</div>');
|
env.write(testTemplateFile, ' \n {{foo}}');
|
||||||
const indexed = env.driveIndexer();
|
const indexed = env.driveIndexer();
|
||||||
const [[_, indexedComp]] = Array.from(indexed.entries());
|
const [[_, indexedComp]] = Array.from(indexed.entries());
|
||||||
const template = indexedComp.template;
|
const template = indexedComp.template;
|
||||||
|
|
||||||
expect(template).toEqual({
|
expect(template).toEqual({
|
||||||
identifiers: new Set([{
|
identifiers: new Set<TopLevelIdentifier>([{
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
kind: IdentifierKind.Property,
|
kind: IdentifierKind.Property,
|
||||||
span: new AbsoluteSourceSpan(12, 15),
|
span: new AbsoluteSourceSpan(7, 10),
|
||||||
}]),
|
}]),
|
||||||
usedComponents: new Set(),
|
usedComponents: new Set(),
|
||||||
isInline: false,
|
isInline: false,
|
||||||
file: new ParseSourceFile('<div> \n {{foo}}</div>', testTemplateFile),
|
file: new ParseSourceFile(' \n {{foo}}', testTemplateFile),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user