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:
parent
3fb73ac62b
commit
74f4f5dfab
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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); }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
]));
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]));
|
||||
});
|
||||
});
|
|
@ -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});
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue