feat(ivy): integrate indexing pipeline with NgtscProgram (#31151)

Add an IndexingContext class to store indexing information and a
transformer module to generate indexing analysis. Integrate the indexing
module with the rest of NgtscProgram and add integration tests.

Closes #30959

PR Close #31151
This commit is contained in:
Ayaz Hafiz 2019-06-19 17:23:59 -07:00 committed by Kara Erickson
parent 3fb73ac62b
commit 74f4f5dfab
19 changed files with 749 additions and 209 deletions

View File

@ -12,6 +12,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",

View File

@ -6,13 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, R3ComponentMetadata, R3TargetBinder, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {IndexingContext} from '../../indexer';
import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata';
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
@ -35,6 +36,7 @@ export interface ComponentHandlerData {
meta: R3ComponentMetadata;
parsedTemplate: TmplAstNode[];
metadataStmt: Statement|null;
parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
}
/**
@ -169,11 +171,22 @@ export class ComponentDecoratorHandler implements
// Parse the template.
// If a preanalyze phase was executed, the template may already exist in parsed form, so check
// the preanalyzeTemplateCache.
let template: ParsedTemplate;
// Extract a closure of the template parsing code so that it can be reparsed with different
// options if needed, like in the indexing pipeline.
let parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
if (this.preanalyzeTemplateCache.has(node)) {
// The template was parsed in preanalyze. Use it and delete it to save memory.
template = this.preanalyzeTemplateCache.get(node) !;
const template = this.preanalyzeTemplateCache.get(node) !;
this.preanalyzeTemplateCache.delete(node);
// A pre-analyzed template cannot be reparsed. Pre-analysis is never run with the indexing
// pipeline.
parseTemplate = (options?: ParseTemplateOptions) => {
if (options !== undefined) {
throw new Error(`Cannot reparse a pre-analyzed template with new options`);
}
return template;
};
} else {
// The template was not already parsed. Either there's a templateUrl, or an inline template.
if (component.has('templateUrl')) {
@ -187,9 +200,9 @@ export class ComponentDecoratorHandler implements
const templateStr = this.resourceLoader.load(templateUrl);
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), templateUrl);
template = this._parseTemplate(
parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
/* escapedString */ false);
/* escapedString */ false, options);
} else {
// Expect an inline template to be present.
const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath);
@ -199,10 +212,11 @@ export class ComponentDecoratorHandler implements
'component is missing a template');
}
const {templateStr, templateUrl, templateRange, escapedString} = inlineTemplate;
template =
this._parseTemplate(component, templateStr, templateUrl, templateRange, escapedString);
parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
component, templateStr, templateUrl, templateRange, escapedString, options);
}
}
const template = parseTemplate();
if (template.errors !== undefined) {
throw new Error(
@ -294,7 +308,7 @@ export class ComponentDecoratorHandler implements
},
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore),
parsedTemplate: template.nodes,
parsedTemplate: template.nodes, parseTemplate,
},
typeCheck: true,
};
@ -304,6 +318,37 @@ export class ComponentDecoratorHandler implements
return output;
}
index(context: IndexingContext, node: ClassDeclaration, analysis: ComponentHandlerData) {
// The component template may have been previously parsed without preserving whitespace or with
// `leadingTriviaChar`s, both of which may manipulate the AST into a form not representative of
// the source code, making it unsuitable for indexing. The template is reparsed with preserving
// options to remedy this.
const template = analysis.parseTemplate({
preserveWhitespaces: true,
leadingTriviaChars: [],
});
const scope = this.scopeRegistry.getScopeForComponent(node);
const selector = analysis.meta.selector;
const matcher = new SelectorMatcher<DirectiveMeta>();
if (scope !== null) {
for (const directive of scope.compilation.directives) {
matcher.addSelectables(CssSelector.parse(directive.selector), directive);
}
}
const binder = new R3TargetBinder(matcher);
const boundTemplate = binder.bind({template: template.nodes});
context.addComponent({
declaration: node,
selector,
boundTemplate,
templateMeta: {
isInline: template.isInline,
file: template.file,
},
});
}
typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: ComponentHandlerData): void {
if (!ts.isClassDeclaration(node)) {
return;
@ -576,7 +621,8 @@ export class ComponentDecoratorHandler implements
private _parseTemplate(
component: Map<string, ts.Expression>, templateStr: string, templateUrl: string,
templateRange: LexerRange|undefined, escapedString: boolean): ParsedTemplate {
templateRange: LexerRange|undefined, escapedString: boolean,
options: ParseTemplateOptions = {}): ParsedTemplate {
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
if (component.has('preserveWhitespaces')) {
const expr = component.get('preserveWhitespaces') !;
@ -602,11 +648,14 @@ export class ComponentDecoratorHandler implements
}
return {
interpolation, ...parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange, escapedString
}),
interpolation,
...parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange, escapedString, ...options,
}),
isInline: component.has('template'),
file: new ParseSourceFile(templateStr, templateUrl),
};
}
@ -668,4 +717,6 @@ interface ParsedTemplate {
nodes: TmplAstNode[];
styleUrls: string[];
styles: string[];
isInline: boolean;
file: ParseSourceFile;
}

View File

@ -9,6 +9,9 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/reflection",
"@npm//typescript",
],
)

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {InterpolationConfig, ParseSourceFile} from '@angular/compiler';
import {ParseTemplateOptions} from '@angular/compiler/src/render3/view/template';
import {ParseSourceFile} from '@angular/compiler';
import * as ts from 'typescript';
/**
@ -33,7 +32,6 @@ export interface TemplateIdentifier {
name: string;
span: AbsoluteSourceSpan;
kind: IdentifierKind;
file: ParseSourceFile;
}
/**
@ -42,21 +40,11 @@ export interface TemplateIdentifier {
export interface IndexedComponent {
name: string;
selector: string|null;
sourceFile: string;
content: string;
file: ParseSourceFile;
template: {
identifiers: Set<TemplateIdentifier>,
usedComponents: Set<ts.ClassDeclaration>,
usedComponents: Set<ts.Declaration>,
isInline: boolean,
file: ParseSourceFile;
};
}
/**
* Options for restoring a parsed template. See `template.ts#restoreTemplate`.
*/
export interface RestoreTemplateOptions extends ParseTemplateOptions {
/**
* The interpolation configuration of the template is lost after it already
* parsed, so it must be respecified.
*/
interpolationConfig: InterpolationConfig;
}

View File

@ -6,8 +6,55 @@
* found in the LICENSE file at https://angular.io/license
*/
import {BoundTarget, DirectiveMeta, ParseSourceFile} from '@angular/compiler';
import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection';
export interface ComponentMeta extends DirectiveMeta {
ref: Reference<ClassDeclaration>;
/**
* Unparsed selector of the directive.
*/
selector: string;
}
/**
* Stores analysis information about components in a compilation for and provides methods for
* querying information about components to be used in indexing.
* An intermediate representation of a component.
*/
export class IndexingContext {}
export interface ComponentInfo {
/** Component TypeScript class declaration */
declaration: ClassDeclaration;
/** Component template selector if it exists, otherwise null. */
selector: string|null;
/**
* BoundTarget containing the parsed template. Can also be used to query for directives used in
* the template.
*/
boundTemplate: BoundTarget<ComponentMeta>;
/** Metadata about the template */
templateMeta: {
/** Whether the component template is inline */
isInline: boolean;
/** Template file recorded by template parser */
file: ParseSourceFile;
};
}
/**
* A context for storing indexing infromation about components of a program.
*
* An `IndexingContext` collects component and template analysis information from
* `DecoratorHandler`s and exposes them to be indexed.
*/
export class IndexingContext {
readonly components = new Set<ComponentInfo>();
/**
* Adds a component to the context.
*/
addComponent(info: ComponentInfo) { this.components.add(info); }
}

View File

@ -6,12 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, HtmlParser, Lexer, MethodCall, ParseSourceFile, PropertyRead, RecursiveAstVisitor, TmplAstNode, TokenType, visitAll} from '@angular/compiler';
import {AST, BoundTarget, DirectiveMeta, ImplicitReceiver, MethodCall, PropertyRead, RecursiveAstVisitor} from '@angular/compiler';
import {BoundText, Element, Node, RecursiveVisitor as RecursiveTemplateVisitor, Template} from '@angular/compiler/src/render3/r3_ast';
import {htmlAstToRender3Ast} from '@angular/compiler/src/render3/r3_template_transform';
import {I18nMetaVisitor} from '@angular/compiler/src/render3/view/i18n/meta';
import {makeBindingParser} from '@angular/compiler/src/render3/view/template';
import {AbsoluteSourceSpan, IdentifierKind, RestoreTemplateOptions, TemplateIdentifier} from './api';
import {AbsoluteSourceSpan, IdentifierKind, TemplateIdentifier} from './api';
/**
* A parsed node in a template, which may have a name (if it is a selector) or
@ -22,46 +19,6 @@ interface HTMLNode extends Node {
name?: string;
}
/**
* Updates the location of an identifier to its real anchor in a source code.
*
* 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.
*
* To remedy all this, the visitor tokenizes the template node the expression was discovered in,
* and updates the locations of entities found during expression traversal with those of the
* tokens.
*
* TODO(ayazhafiz): Think about how to handle `PropertyRead`s in `BoundAttribute`s. The Lexer
* tokenizes the attribute as a string and ignores quotes.
*
* @param entities entities to update
* @param currentNode node expression was in
*/
function updateIdentifierSpans(identifiers: TemplateIdentifier[], currentNode: Node) {
const localSpan = currentNode.sourceSpan;
const localExpression = localSpan.toString();
const lexedIdentifiers =
new Lexer().tokenize(localExpression).filter(token => token.type === TokenType.Identifier);
// Join the relative position of the expression within a node with the absolute position of the
// node to get the absolute position of the expression in the source code.
const absoluteOffset = currentNode.sourceSpan.start.offset;
identifiers.forEach((id, index) => {
const lexedId = lexedIdentifiers[index];
if (id.name !== lexedId.strValue) {
throw new Error(
'Impossible state: lexed and parsed expression should contain the same tokens.');
}
const start = absoluteOffset + lexedId.index;
const absoluteSpan = new AbsoluteSourceSpan(start, start + lexedId.strValue.length);
id.span = absoluteSpan;
});
}
/**
* Visits the AST of an Angular template syntax expression, finding interesting
* entities (variable references, etc.). Creates an array of Entities found in
@ -72,41 +29,74 @@ function updateIdentifierSpans(identifiers: TemplateIdentifier[], currentNode: N
* 11}}]`.
*/
class ExpressionVisitor extends RecursiveAstVisitor {
private readonly file: ParseSourceFile;
readonly identifiers: TemplateIdentifier[] = [];
private constructor(context: Node, readonly identifiers: TemplateIdentifier[] = []) {
private constructor(
context: Node, private readonly boundTemplate: BoundTarget<DirectiveMeta>,
private readonly expressionStr = context.sourceSpan.toString(),
private readonly absoluteOffset = context.sourceSpan.start.offset) {
super();
this.file = context.sourceSpan.start.file;
}
static getIdentifiers(ast: AST, context: Node): TemplateIdentifier[] {
const visitor = new ExpressionVisitor(context);
/**
* Returns identifiers discovered in an expression.
*
* @param ast expression AST to visit
* @param context HTML node expression is defined in
* @param boundTemplate bound target of the entire template, which can be used to query for the
* entities expressions target.
*/
static getIdentifiers(ast: AST, context: Node, boundTemplate: BoundTarget<DirectiveMeta>):
TemplateIdentifier[] {
const visitor = new ExpressionVisitor(context, boundTemplate);
visitor.visit(ast);
const identifiers = visitor.identifiers;
updateIdentifierSpans(identifiers, context);
return identifiers;
return visitor.identifiers;
}
visit(ast: AST) { ast.visit(this); }
visitMethodCall(ast: MethodCall, context: {}) {
this.addIdentifier(ast, IdentifierKind.Method);
this.visitIdentifier(ast, IdentifierKind.Method);
super.visitMethodCall(ast, context);
}
visitPropertyRead(ast: PropertyRead, context: {}) {
this.addIdentifier(ast, IdentifierKind.Property);
this.visitIdentifier(ast, IdentifierKind.Property);
super.visitPropertyRead(ast, context);
}
private addIdentifier(ast: AST&{name: string}, kind: IdentifierKind) {
/**
* Visits an identifier, adding it to the identifier store if it is useful for indexing.
*
* @param ast expression AST the identifier is in
* @param kind identifier kind
*/
private visitIdentifier(ast: AST&{name: string, receiver: AST}, kind: IdentifierKind) {
// 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.
// 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) {
return;
}
// Get the location of the identifier of real interest.
// 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 identifierStart = ast.span.start + localExpression.indexOf(ast.name);
// Join the relative position of the expression within a node with the absolute position
// of the node to get the absolute position of the expression in the source code.
const absoluteStart = this.absoluteOffset + identifierStart;
const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length);
this.identifiers.push({
name: ast.name,
span: ast.span, kind,
file: this.file,
span,
kind,
});
}
}
@ -119,8 +109,17 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
// identifiers of interest found in the template
readonly identifiers = new Set<TemplateIdentifier>();
/**
* 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.
*
* @param boundTemplate bound template target
*/
constructor(private boundTemplate: BoundTarget<DirectiveMeta>) { super(); }
/**
* Visits a node in the template.
*
* @param node node to visit
*/
visit(node: HTMLNode) { node.visit(this); }
@ -138,72 +137,30 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
this.visitAll(template.references);
this.visitAll(template.variables);
}
visitBoundText(text: BoundText) { this.addIdentifiers(text); }
visitBoundText(text: BoundText) { this.visitExpression(text); }
/**
* Adds identifiers to the visitor's state.
* @param visitedEntities interesting entities to add as identifiers
* @param curretNode node entities were discovered in
* Visits a node's expression and adds its identifiers, if any, to the visitor's state.
*
* @param curretNode node whose expression to visit
*/
private addIdentifiers(node: Node&{value: AST}) {
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node);
private visitExpression(node: Node&{value: AST}) {
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node, this.boundTemplate);
identifiers.forEach(id => this.identifiers.add(id));
}
}
/**
* Unwraps and reparses a template, preserving whitespace and with no leading trivial characters.
*
* A template may previously have been parsed without preserving whitespace, and was definitely
* parsed with leading trivial characters (see `parseTemplate` from the compiler package API).
* Both of these are detrimental for indexing as they result in a manipulated AST not representing
* the template source code.
*
* TODO(ayazhafiz): Remove once issues with `leadingTriviaChars` and `parseTemplate` are resolved.
*/
function restoreTemplate(template: TmplAstNode[], options: RestoreTemplateOptions): TmplAstNode[] {
// try to recapture the template content and URL
// if there was nothing in the template to begin with, this is just a no-op
if (template.length === 0) {
return [];
}
const {content: templateStr, url: templateUrl} = template[0].sourceSpan.start.file;
options.preserveWhitespaces = true;
const {interpolationConfig, preserveWhitespaces} = options;
const bindingParser = makeBindingParser(interpolationConfig);
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(templateStr, templateUrl, {
...options,
tokenizeExpansionForms: true,
});
if (parseResult.errors && parseResult.errors.length > 0) {
throw new Error('Impossible state: template must have been successfully parsed previously.');
}
const rootNodes = visitAll(
new I18nMetaVisitor(interpolationConfig, !preserveWhitespaces), parseResult.rootNodes);
const {nodes, errors} = htmlAstToRender3Ast(rootNodes, bindingParser);
if (errors && errors.length > 0) {
throw new Error('Impossible state: template must have been successfully parsed previously.');
}
return nodes;
}
/**
* Traverses a template AST and builds identifiers discovered in it.
* @param template template to extract indentifiers from
* @param options options for restoring the parsed template to a indexable state
*
* @param boundTemplate bound template target, which can be used for querying expression targets.
* @return identifiers in template
*/
export function getTemplateIdentifiers(
template: TmplAstNode[], options: RestoreTemplateOptions): Set<TemplateIdentifier> {
const restoredTemplate = restoreTemplate(template, options);
const visitor = new TemplateVisitor();
visitor.visitAll(restoredTemplate);
export function getTemplateIdentifiers(boundTemplate: BoundTarget<DirectiveMeta>):
Set<TemplateIdentifier> {
const visitor = new TemplateVisitor(boundTemplate);
if (boundTemplate.target.template !== undefined) {
visitor.visitAll(boundTemplate.target.template);
}
return visitor.identifiers;
}

View File

@ -6,9 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceFile} from '@angular/compiler/src/compiler';
import * as ts from 'typescript';
import {IndexedComponent} from './api';
import {IndexingContext} from './context';
import {getTemplateIdentifiers} from './template';
/**
* Generates `IndexedComponent` entries from a `IndexingContext`, which has information
@ -17,5 +19,42 @@ import {IndexingContext} from './context';
* The context must be populated before `generateAnalysis` is called.
*/
export function generateAnalysis(context: IndexingContext): Map<ts.Declaration, IndexedComponent> {
throw new Error('Method not implemented.');
const analysis = new Map<ts.Declaration, IndexedComponent>();
context.components.forEach(({declaration, selector, boundTemplate, templateMeta}) => {
const name = declaration.name.getText();
const usedComponents = new Set<ts.Declaration>();
const usedDirs = boundTemplate.getUsedDirectives();
usedDirs.forEach(dir => {
if (dir.isComponent) {
usedComponents.add(dir.ref.node);
}
});
// Get source files for the component and the template. If the template is inline, its source
// file is the component's.
const componentFile = new ParseSourceFile(
declaration.getSourceFile().getFullText(), declaration.getSourceFile().fileName);
let templateFile: ParseSourceFile;
if (templateMeta.isInline) {
templateFile = componentFile;
} else {
templateFile = templateMeta.file;
}
analysis.set(declaration, {
name,
selector,
file: componentFile,
template: {
identifiers: getTemplateIdentifiers(boundTemplate),
usedComponents,
isInline: templateMeta.isInline,
file: templateFile,
},
});
});
return analysis;
}

View File

@ -10,7 +10,11 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript",
],
)

View File

@ -0,0 +1,39 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {ParseSourceFile} from '@angular/compiler';
import {IndexingContext} from '../src/context';
import * as util from './util';
describe('ComponentAnalysisContext', () => {
it('should store and return information about components', () => {
const context = new IndexingContext();
const declaration = util.getComponentDeclaration('class C {};', 'C');
const boundTemplate = util.getBoundTemplate('<div></div>');
context.addComponent({
declaration,
selector: 'c-selector', boundTemplate,
templateMeta: {
isInline: false,
file: new ParseSourceFile('<div></div>', util.TESTFILE),
},
});
expect(context.components).toEqual(new Set([
{
declaration,
selector: 'c-selector', boundTemplate,
templateMeta: {
isInline: false,
file: new ParseSourceFile('<div></div>', util.TESTFILE),
},
},
]));
});
});

View File

@ -6,93 +6,123 @@
* found in the LICENSE file at https://angular.io/license
*/
import {InterpolationConfig, ParseSourceFile, TmplAstNode, parseTemplate} from '@angular/compiler';
import {AbsoluteSourceSpan, IdentifierKind, RestoreTemplateOptions} from '..';
import {AbsoluteSourceSpan, IdentifierKind} from '..';
import {getTemplateIdentifiers} from '../src/template';
import * as util from './util';
const TEST_FILE = 'TEST';
function parse(template: string): TmplAstNode[] {
return parseTemplate(template, TEST_FILE).nodes;
function bind(template: string) {
return util.getBoundTemplate(template, {
preserveWhitespaces: true,
leadingTriviaChars: [],
});
}
describe('getTemplateIdentifiers', () => {
const DEFAULT_RESTORE_OPTIONS:
RestoreTemplateOptions = {interpolationConfig: new InterpolationConfig('{{', '}}')};
it('should generate nothing in HTML-only template', () => {
const refs = getTemplateIdentifiers(parse('<div></div>'), DEFAULT_RESTORE_OPTIONS);
const refs = getTemplateIdentifiers(bind('<div></div>'));
expect(refs.size).toBe(0);
});
it('should ignore comments', () => {
const refs = getTemplateIdentifiers(
parse(`
<!-- {{my_module}} -->
<div><!-- {{goodbye}} --></div>
`),
DEFAULT_RESTORE_OPTIONS);
const refs = getTemplateIdentifiers(bind('<!-- {{comment}} -->'));
expect(refs.size).toBe(0);
});
it('should use any interpolation config', () => {
const template = '<div>((foo))</div>';
const refs = getTemplateIdentifiers(
parse(template), {interpolationConfig: new InterpolationConfig('((', '))')});
it('should handle arbitrary whitespace', () => {
const template = '<div>\n\n {{foo}}</div>';
const refs = getTemplateIdentifiers(bind(template));
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
expect(ref.kind).toBe(IdentifierKind.Property);
expect(ref.span).toEqual(new AbsoluteSourceSpan(7, 10));
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
expect(ref).toEqual({
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
});
});
it('should ignore identifiers defined in the template', () => {
const template = `
<input #model />
{{model.valid}}
`;
const refs = getTemplateIdentifiers(bind(template));
const refArr = Array.from(refs);
const modelId = refArr.find(ref => ref.name === 'model');
expect(modelId).toBeUndefined();
});
describe('generates identifiers for PropertyReads', () => {
it('should discover component properties', () => {
const template = '<div>{{foo}}</div>';
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
const template = '{{foo}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref).toEqual({
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
});
});
it('should discover nested properties', () => {
const template = '<div><span>{{foo}}</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(13, 16),
}]));
});
it('should ignore identifiers that are not implicitly received by the template', () => {
const template = '{{foo.bar.baz}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
expect(ref.kind).toBe(IdentifierKind.Property);
expect(ref.span).toEqual(new AbsoluteSourceSpan(7, 10));
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
});
});
describe('generates identifiers for MethodCalls', () => {
it('should discover component method calls', () => {
const template = '<div>{{foo()}}</div>';
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
const template = '{{foo()}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
expect(ref.kind).toBe(IdentifierKind.Method);
expect(ref.span).toEqual(new AbsoluteSourceSpan(7, 10));
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
expect(ref).toEqual({
name: 'foo',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(2, 5),
});
});
it('should handle arbitrary whitespace', () => {
const template = '<div>\n\n {{foo}}</div>';
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
it('should discover nested properties', () => {
const template = '<div><span>{{foo()}}</span></div>';
const refs = getTemplateIdentifiers(bind(template));
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
expect(ref.kind).toBe(IdentifierKind.Property);
expect(ref.span).toEqual(new AbsoluteSourceSpan(12, 15));
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
const refArr = Array.from(refs);
expect(refArr).toEqual(jasmine.arrayContaining([{
name: 'foo',
kind: IdentifierKind.Method,
span: new AbsoluteSourceSpan(13, 16),
}]));
});
it('should handle nested scopes', () => {
const template = '<div><span>{{foo}}</span></div>';
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
it('should ignore identifiers that are not implicitly received by the template', () => {
const template = '{{foo().bar().baz()}}';
const refs = getTemplateIdentifiers(bind(template));
expect(refs.size).toBe(1);
const [ref] = Array.from(refs);
expect(ref.name).toBe('foo');
expect(ref.kind).toBe(IdentifierKind.Property);
expect(ref.span).toEqual(new AbsoluteSourceSpan(13, 16));
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
});
});
});

View File

@ -0,0 +1,117 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {BoundTarget, ParseSourceFile} from '@angular/compiler';
import {DirectiveMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {IndexingContext} from '../src/context';
import {getTemplateIdentifiers} from '../src/template';
import {generateAnalysis} from '../src/transform';
import * as util from './util';
/**
* Adds information about a component to a context.
*/
function populateContext(
context: IndexingContext, component: ClassDeclaration, selector: string, template: string,
boundTemplate: BoundTarget<DirectiveMeta>, isInline: boolean = false) {
context.addComponent({
declaration: component,
selector,
boundTemplate,
templateMeta: {
isInline,
file: new ParseSourceFile(template, util.TESTFILE),
},
});
}
describe('generateAnalysis', () => {
it('should emit component and template analysis information', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toEqual({
name: 'C',
selector: 'c-selector',
file: new ParseSourceFile('class C {}', util.TESTFILE),
template: {
identifiers: getTemplateIdentifiers(util.getBoundTemplate('<div>{{foo}}</div>')),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('<div>{{foo}}</div>', util.TESTFILE),
}
});
});
it('should give inline templates the component source file', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(
context, decl, 'c-selector', '<div>{{foo}}</div>', util.getBoundTemplate(template),
/* inline template */ true);
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toBeDefined();
expect(info !.template.file).toEqual(new ParseSourceFile('class C {}', util.TESTFILE));
});
it('should give external templates their own source file', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toBeDefined();
expect(info !.template.file).toEqual(new ParseSourceFile('<div>{{foo}}</div>', util.TESTFILE));
});
it('should emit used components', () => {
const context = new IndexingContext();
const templateA = '<b-selector></b-selector>';
const declA = util.getComponentDeclaration('class A {}', 'A');
const templateB = '<a-selector></a-selector>';
const declB = util.getComponentDeclaration('class B {}', 'B');
const boundA =
util.getBoundTemplate(templateA, {}, [{selector: 'b-selector', declaration: declB}]);
const boundB =
util.getBoundTemplate(templateB, {}, [{selector: 'a-selector', declaration: declA}]);
populateContext(context, declA, 'a-selector', templateA, boundA);
populateContext(context, declB, 'b-selector', templateB, boundB);
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(2);
const infoA = analysis.get(declA);
expect(infoA).toBeDefined();
expect(infoA !.template.usedComponents).toEqual(new Set([declB]));
const infoB = analysis.get(declB);
expect(infoB).toBeDefined();
expect(infoB !.template.usedComponents).toEqual(new Set([declA]));
});
});

View File

@ -0,0 +1,61 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {BoundTarget, CssSelector, ParseTemplateOptions, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {DirectiveMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
/** Dummy file URL */
export const TESTFILE = '/TESTFILE.ts';
/**
* Creates a class declaration from a component source code.
*/
export function getComponentDeclaration(componentStr: string, className: string): ClassDeclaration {
const program = makeProgram([{name: TESTFILE, contents: componentStr}]);
return getDeclaration(
program.program, TESTFILE, className,
(value: ts.Declaration): value is ClassDeclaration => ts.isClassDeclaration(value));
}
/**
* Parses a template source code and returns a template-bound target, optionally with information
* about used components.
*
* @param template template to parse
* @param options extra template parsing options
* @param components components to bind to the template target
*/
export function getBoundTemplate(
template: string, options: ParseTemplateOptions = {},
components: Array<{selector: string, declaration: ClassDeclaration}> =
[]): BoundTarget<DirectiveMeta> {
const matcher = new SelectorMatcher<DirectiveMeta>();
components.forEach(({selector, declaration}) => {
matcher.addSelectables(CssSelector.parse(selector), {
ref: new Reference(declaration),
selector,
queries: [],
ngTemplateGuards: [],
hasNgTemplateContextGuard: false,
baseClass: null,
name: declaration.name.getText(),
isComponent: true,
inputs: {},
outputs: {},
exportAs: null,
});
});
const binder = new R3TargetBinder(matcher);
return binder.bind({template: parseTemplate(template, TESTFILE, options).nodes});
}

View File

@ -19,7 +19,8 @@ import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {IncrementalState} from './incremental';
import {IndexedComponent} from './indexer';
import {IndexedComponent, IndexingContext} from './indexer';
import {generateAnalysis} from './indexer/src/transform';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata';
import {PartialEvaluator} from './partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from './path';
@ -268,10 +269,6 @@ export class NgtscProgram implements api.Program {
return this.routeAnalyzer !.listLazyRoutes(entryRoute);
}
getIndexedComponents(): Map<ts.Declaration, IndexedComponent> {
throw new Error('Method not implemented.');
}
getLibrarySummaries(): Map<string, api.LibrarySummary> {
throw new Error('Method not implemented.');
}
@ -434,6 +431,13 @@ export class NgtscProgram implements api.Program {
return diagnostics;
}
getIndexedComponents(): Map<ts.Declaration, IndexedComponent> {
const compilation = this.ensureAnalyzed();
const context = new IndexingContext();
compilation.index(context);
return generateAnalysis(context);
}
private makeCompilation(): IvyCompilation {
const checker = this.tsProgram.getTypeChecker();

View File

@ -82,7 +82,7 @@ export interface DecoratorHandler<A, M> {
* `IndexingContext`, which stores information about components discovered in the
* program.
*/
index?(context: IndexingContext, node: ClassDeclaration, metadata: M): void;
index?(context: IndexingContext, node: ClassDeclaration, metadata: A): void;
/**
* Perform resolution on the given decorator along with the result of analysis.

View File

@ -254,7 +254,16 @@ export class IvyCompilation {
/**
* Feeds components discovered in the compilation to a context for indexing.
*/
index(context: IndexingContext) { throw new Error('Method not implemented.'); }
index(context: IndexingContext) {
this.ivyClasses.forEach((ivyClass, declaration) => {
for (const match of ivyClass.matchedHandlers) {
if (match.handler.index !== undefined && match.analyzed !== null &&
match.analyzed.analysis !== undefined) {
match.handler.index(context, declaration, match.analyzed.analysis);
}
}
});
}
resolve(): void {
const resolveSpan = this.perf.start('resolve');

View File

@ -8,6 +8,7 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/util",

View File

@ -0,0 +1,180 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AbsoluteSourceSpan, IdentifierKind} from '@angular/compiler-cli/src/ngtsc/indexer';
import {ParseSourceFile} from '@angular/compiler/src/compiler';
import * as path from 'path';
import {NgtscTestEnvironment} from './env';
describe('ngtsc component indexing', () => {
let env !: NgtscTestEnvironment;
function testPath(testFile: string): string { return path.posix.join(env.basePath, testFile); }
beforeEach(() => {
env = NgtscTestEnvironment.setup();
env.tsconfig();
});
describe('indexing metadata', () => {
it('should generate component metadata', () => {
const componentContent = `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
template: '<div></div>',
})
export class TestCmp {}
`;
env.write('test.ts', componentContent);
const indexed = env.driveIndexer();
expect(indexed.size).toBe(1);
const [[decl, indexedComp]] = Array.from(indexed.entries());
expect(decl.getText()).toContain('export class TestCmp {}');
expect(indexedComp).toEqual(jasmine.objectContaining({
name: 'TestCmp',
selector: 'test-cmp',
file: new ParseSourceFile(componentContent, testPath('test.ts')),
}));
});
it('should index inline templates', () => {
const componentContent = `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
template: '{{foo}}',
})
export class TestCmp { foo = 0; }
`;
env.write('test.ts', componentContent);
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(127, 130),
}]),
usedComponents: new Set(),
isInline: true,
file: new ParseSourceFile(componentContent, testPath('test.ts')),
});
});
it('should index external templates', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
templateUrl: './test.html',
})
export class TestCmp { foo = 0; }
`);
env.write('test.html', '{{foo}}');
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
}]),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('{{foo}}', testPath('test.html')),
});
});
it('should index templates compiled without preserving whitespace', () => {
env.tsconfig({
preserveWhitespaces: false,
});
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
templateUrl: './test.html',
})
export class TestCmp { foo = 0; }
`);
env.write('test.html', '<div> \n {{foo}}</div>');
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
}]),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('<div> \n {{foo}}</div>', testPath('test.html')),
});
});
it('should generated information about used components', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
templateUrl: './test.html',
})
export class TestCmp {}
`);
env.write('test.html', '<div></div>');
env.write('test_import.ts', `
import {Component, NgModule} from '@angular/core';
import {TestCmp} from './test';
@Component({
templateUrl: './test_import.html',
})
export class TestImportCmp {}
@NgModule({
declarations: [
TestCmp,
TestImportCmp,
],
bootstrap: [TestImportCmp]
})
export class TestModule {}
`);
env.write('test_import.html', '<test-cmp></test-cmp>');
const indexed = env.driveIndexer();
expect(indexed.size).toBe(2);
const indexedComps = Array.from(indexed.values());
const testComp = indexedComps.find(comp => comp.name === 'TestCmp');
const testImportComp = indexedComps.find(cmp => cmp.name === 'TestImportCmp');
expect(testComp).toBeDefined();
expect(testImportComp).toBeDefined();
expect(testComp !.template.usedComponents.size).toBe(0);
expect(testImportComp !.template.usedComponents.size).toBe(1);
const [usedComp] = Array.from(testImportComp !.template.usedComponents);
expect(indexed.get(usedComp)).toEqual(testComp);
});
});
});

View File

@ -7,6 +7,8 @@
*/
import {CustomTransformers, Program} from '@angular/compiler-cli';
import {IndexedComponent} from '@angular/compiler-cli/src/ngtsc/indexer';
import {NgtscProgram} from '@angular/compiler-cli/src/ngtsc/program';
import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
import * as fs from 'fs';
import * as path from 'path';
@ -203,6 +205,13 @@ export class NgtscTestEnvironment {
const program = createProgram({rootNames, host, options});
return program.listLazyRoutes(entryPoint);
}
driveIndexer(): Map<ts.Declaration, IndexedComponent> {
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
const host = createCompilerHost({options});
const program = createProgram({rootNames, host, options});
return (program as NgtscProgram).getIndexedComponents();
}
}
class AugmentedCompilerHost {

View File

@ -97,7 +97,7 @@ export {Identifiers as R3Identifiers} from './render3/r3_identifiers';
export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler';
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
export {makeBindingParser, parseTemplate} from './render3/view/template';
export {makeBindingParser, parseTemplate, ParseTemplateOptions} from './render3/view/template';
export {R3Reference} from './render3/util';
export {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler';
export {publishFacade} from './jit_compiler_facade';