From 5b32f55a3a4ff6340d1c2a7f3a99a3bf8bcf36f4 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 8 Apr 2019 16:16:56 +0200 Subject: [PATCH] refactor(core): static-query schematic should check templates (#29713) Queries can technically be also accessed within component templates e.g. ```html ``` In that case the query with the property "myQuery" is accessed statically and needs to be marked with `static: true`. There are other edge cases that need to be handled as the template property read doesn't necessarily resolve to the actual query property. For example: ```html ``` In this scenario the binding doesn't refer to the actual query because the template reference variable takes precedence. The query doesn't need to be marked with "static: true" this time. This commit ensures that the `static-query` migration schematic now handles this cases properly. Also template property reads that access queries from within a `` are ignored as these can't access the query before the view has been initialized. Resolves FW-1216 PR Close #29713 --- .../migrations/static-queries/BUILD.bazel | 1 + .../angular/analyze_query_usage.ts | 17 +++ .../angular/ng_query_visitor.ts | 8 +- .../angular/query_read_html_visitor.ts | 88 +++++++++++ .../static-queries/google3/BUILD.bazel | 1 + .../google3/explicitQueryTimingRule.ts | 22 ++- .../migrations/static-queries/index.ts | 21 ++- .../analyze_template.ts | 28 ++-- .../noTemplateVariableAssignmentRule.ts | 7 +- .../template-var-assignment/index.ts | 5 +- .../explicit_query_timing_rule_spec.ts | 20 +++ .../test/static_queries_migration_spec.ts | 138 ++++++++++++++++++ packages/core/schematics/utils/BUILD.bazel | 1 + .../schematics/utils/ng_component_template.ts | 19 ++- packages/core/schematics/utils/parse_html.ts | 25 ++++ .../utils/typescript/visit_nodes.ts | 16 ++ 16 files changed, 385 insertions(+), 32 deletions(-) create mode 100644 packages/core/schematics/migrations/static-queries/angular/query_read_html_visitor.ts create mode 100644 packages/core/schematics/utils/parse_html.ts create mode 100644 packages/core/schematics/utils/typescript/visit_nodes.ts diff --git a/packages/core/schematics/migrations/static-queries/BUILD.bazel b/packages/core/schematics/migrations/static-queries/BUILD.bazel index d5211ce3b1..aa2c77ec83 100644 --- a/packages/core/schematics/migrations/static-queries/BUILD.bazel +++ b/packages/core/schematics/migrations/static-queries/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( "//packages/core/schematics/test:__pkg__", ], deps = [ + "//packages/compiler", "//packages/core/schematics/utils", "@npm//@angular-devkit/schematics", "@npm//@types/node", diff --git a/packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts b/packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts index a75f9ef5b1..64f2dbad5f 100644 --- a/packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts +++ b/packages/core/schematics/migrations/static-queries/angular/analyze_query_usage.ts @@ -7,13 +7,17 @@ */ import * as ts from 'typescript'; + +import {parseHtmlGracefully} from '../../../utils/parse_html'; import {hasPropertyNameText} from '../../../utils/typescript/property_name'; import {DeclarationUsageVisitor, FunctionContext} from './declaration_usage_visitor'; import {ClassMetadataMap} from './ng_query_visitor'; import {NgQueryDefinition, QueryTiming, QueryType} from './query-definition'; +import {QueryReadHtmlVisitor} from './query_read_html_visitor'; import {updateSuperClassAbstractMembersContext} from './super_class'; + /** * Object that maps a given type of query to a list of lifecycle hooks that * could be used to access such a query statically. @@ -69,6 +73,19 @@ function isQueryUsedStatically( return false; } + // In case there is a component template for the current class, we check if the + // template statically accesses the current query. In case that's true, the query + // can be marked as static. + if (classMetadata.template && hasPropertyNameText(query.property.name)) { + const template = classMetadata.template; + const parsedHtml = parseHtmlGracefully(template.content, template.filePath); + const htmlVisitor = new QueryReadHtmlVisitor(query.property.name.text); + + if (parsedHtml && htmlVisitor.isQueryUsedStatically(parsedHtml)) { + return true; + } + } + // In case derived classes should also be analyzed, we determine the classes that derive // from the current class and check if these have input setters or lifecycle hooks that // use the query statically. diff --git a/packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts b/packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts index f6d4130762..9b77c61e43 100644 --- a/packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts +++ b/packages/core/schematics/migrations/static-queries/angular/ng_query_visitor.ts @@ -7,11 +7,15 @@ */ import * as ts from 'typescript'; + +import {ResolvedTemplate} from '../../../utils/ng_component_template'; import {getAngularDecorators} from '../../../utils/ng_decorators'; import {findParentClassDeclaration, getBaseTypeIdentifiers} from '../../../utils/typescript/class_declaration'; + import {getInputNamesOfClass} from './directive_inputs'; import {NgQueryDefinition, QueryType} from './query-definition'; + /** Resolved metadata of a given class. */ export interface ClassMetadata { /** List of class declarations that derive from the given class. */ @@ -20,6 +24,8 @@ export interface ClassMetadata { superClass: ts.ClassDeclaration|null; /** List of property names that declare an Angular input within the given class. */ ngInputNames: string[]; + /** Component template that belongs to that class if present. */ + template?: ResolvedTemplate; } /** Type that describes a map which can be used to get a class declaration's metadata. */ @@ -48,8 +54,6 @@ export class NgQueryResolveVisitor { this.visitClassDeclaration(node as ts.ClassDeclaration); break; } - - ts.forEachChild(node, node => this.visitNode(node)); } private visitPropertyDeclaration(node: ts.PropertyDeclaration) { diff --git a/packages/core/schematics/migrations/static-queries/angular/query_read_html_visitor.ts b/packages/core/schematics/migrations/static-queries/angular/query_read_html_visitor.ts new file mode 100644 index 0000000000..7ff96a436d --- /dev/null +++ b/packages/core/schematics/migrations/static-queries/angular/query_read_html_visitor.ts @@ -0,0 +1,88 @@ +/** + * @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 {ImplicitReceiver, ParseSourceSpan, PropertyRead, RecursiveAstVisitor} from '@angular/compiler'; +import {BoundAttribute, BoundEvent, BoundText, Element, Node, NullVisitor, Template, visitAll} from '@angular/compiler/src/render3/r3_ast'; + +/** + * AST visitor that traverses the Render3 HTML AST in order to check if the given + * query property is accessed statically in the template. + */ +export class QueryReadHtmlVisitor extends NullVisitor { + private hasQueryTemplateReference = false; + private expressionAstVisitor = new ExpressionAstVisitor(this.queryPropertyName); + + constructor(public queryPropertyName: string) { super(); } + + /** Checks whether the given query is statically accessed within the specified HTML nodes. */ + isQueryUsedStatically(htmlNodes: Node[]): boolean { + this.hasQueryTemplateReference = false; + this.expressionAstVisitor.hasQueryPropertyRead = false; + + // Visit all AST nodes and check if the query property is used statically. + visitAll(this, htmlNodes); + + return !this.hasQueryTemplateReference && this.expressionAstVisitor.hasQueryPropertyRead; + } + + visitElement(element: Element): void { + // In case there is a template references variable that matches the query property + // name, we can finish this visitor as such a template variable can be used in the + // entire template and the query therefore can't be accessed from the template. + if (element.references.some(r => r.name === this.queryPropertyName)) { + this.hasQueryTemplateReference = true; + return; + } + + visitAll(this, element.attributes); + visitAll(this, element.inputs); + visitAll(this, element.outputs); + visitAll(this, element.children); + } + + visitTemplate(template: Template): void { + visitAll(this, template.attributes); + visitAll(this, template.inputs); + visitAll(this, template.outputs); + + // We don't want to visit any children of the template as these never can't + // access a query statically. The templates can be rendered in the ngAfterViewInit" + // lifecycle hook at the earliest. + } + + visitBoundAttribute(attribute: BoundAttribute) { + attribute.value.visit(this.expressionAstVisitor, attribute.sourceSpan); + } + + visitBoundText(text: BoundText) { text.value.visit(this.expressionAstVisitor, text.sourceSpan); } + + visitBoundEvent(node: BoundEvent) { + node.handler.visit(this.expressionAstVisitor, node.handlerSpan); + } +} + +/** + * AST visitor that checks if the given expression contains property reads that + * refer to the specified query property name. + */ +class ExpressionAstVisitor extends RecursiveAstVisitor { + hasQueryPropertyRead = false; + + constructor(private queryPropertyName: string) { super(); } + + visitPropertyRead(node: PropertyRead, span: ParseSourceSpan): any { + // The receiver of the property read needs to be "implicit" as queries are accessed + // from the component instance and not from other objects. + if (node.receiver instanceof ImplicitReceiver && node.name === this.queryPropertyName) { + this.hasQueryPropertyRead = true; + return; + } + + super.visitPropertyRead(node, span); + } +} diff --git a/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel b/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel index 05a86b63ad..a588549552 100644 --- a/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel @@ -7,6 +7,7 @@ ts_library( visibility = ["//packages/core/schematics/test:__pkg__"], deps = [ "//packages/core/schematics/migrations/static-queries", + "//packages/core/schematics/utils", "@npm//tslint", ], ) diff --git a/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts b/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts index 9af92ee47e..e5d26a9634 100644 --- a/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts +++ b/packages/core/schematics/migrations/static-queries/google3/explicitQueryTimingRule.ts @@ -8,6 +8,9 @@ import {Replacement, RuleFailure, Rules} from 'tslint'; import * as ts from 'typescript'; + +import {NgComponentTemplateVisitor} from '../../../utils/ng_component_template'; +import {visitAllNodes} from '../../../utils/typescript/visit_nodes'; import {analyzeNgQueryUsage} from '../angular/analyze_query_usage'; import {NgQueryResolveVisitor} from '../angular/ng_query_visitor'; import {QueryTiming} from '../angular/query-definition'; @@ -25,14 +28,29 @@ export class Rule extends Rules.TypedRule { applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { const typeChecker = program.getTypeChecker(); const queryVisitor = new NgQueryResolveVisitor(program.getTypeChecker()); + const templateVisitor = new NgComponentTemplateVisitor(typeChecker); const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !); const printer = ts.createPrinter(); const failures: RuleFailure[] = []; - // Analyze source files by detecting queries and class relations. - rootSourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile)); + // Analyze source files by detecting queries, class relations and component templates. + rootSourceFiles.forEach(sourceFile => { + // The visit utility function only traverses the source file once. We don't want to + // traverse through all source files multiple times for each visitor as this could be + // slow. + visitAllNodes(sourceFile, [queryVisitor, templateVisitor]); + }); const {resolvedQueries, classMetadata} = queryVisitor; + + // Add all resolved templates to the class metadata so that we can also + // check component templates for static query usage. + templateVisitor.resolvedTemplates.forEach(template => { + if (classMetadata.has(template.container)) { + classMetadata.get(template.container) !.template = template; + } + }); + const queries = resolvedQueries.get(sourceFile); // No queries detected for the given source file. diff --git a/packages/core/schematics/migrations/static-queries/index.ts b/packages/core/schematics/migrations/static-queries/index.ts index b8cd5c119c..727820792a 100644 --- a/packages/core/schematics/migrations/static-queries/index.ts +++ b/packages/core/schematics/migrations/static-queries/index.ts @@ -10,14 +10,17 @@ import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics'; import {dirname, relative} from 'path'; import * as ts from 'typescript'; +import {NgComponentTemplateVisitor} from '../../utils/ng_component_template'; import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig'; +import {visitAllNodes} from '../../utils/typescript/visit_nodes'; import {analyzeNgQueryUsage} from './angular/analyze_query_usage'; import {NgQueryResolveVisitor} from './angular/ng_query_visitor'; import {getTransformedQueryCallExpr} from './transform'; + /** Entry point for the V8 static-query migration. */ export default function(): Rule { return (tree: Tree) => { @@ -58,14 +61,28 @@ function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: str const program = ts.createProgram(parsed.fileNames, parsed.options, host); const typeChecker = program.getTypeChecker(); const queryVisitor = new NgQueryResolveVisitor(typeChecker); + const templateVisitor = new NgComponentTemplateVisitor(typeChecker); const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !); const printer = ts.createPrinter(); - // Analyze source files by detecting queries and class relations. - rootSourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile)); + // Analyze source files by detecting queries, class relations and component templates. + rootSourceFiles.forEach(sourceFile => { + // The visit utility function only traverses the source file once. We don't want to + // traverse through all source files multiple times for each visitor as this could be + // slow. + visitAllNodes(sourceFile, [queryVisitor, templateVisitor]); + }); const {resolvedQueries, classMetadata} = queryVisitor; + // Add all resolved templates to the class metadata so that we can also + // check component templates for static query usage. + templateVisitor.resolvedTemplates.forEach(template => { + if (classMetadata.has(template.container)) { + classMetadata.get(template.container) !.template = template; + } + }); + // Walk through all source files that contain resolved queries and update // the source files if needed. Note that we need to update multiple queries // within a source file within the same recorder in order to not throw off diff --git a/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts b/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts index 77eb53dfbb..f732c7b760 100644 --- a/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts +++ b/packages/core/schematics/migrations/template-var-assignment/analyze_template.ts @@ -6,10 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {PropertyWrite, parseTemplate} from '@angular/compiler'; +import {PropertyWrite} from '@angular/compiler'; import {Variable, visitAll} from '@angular/compiler/src/render3/r3_ast'; import {ResolvedTemplate} from '../../utils/ng_component_template'; +import {parseHtmlGracefully} from '../../utils/parse_html'; + import {PropertyAssignment, PropertyWriteHtmlVisitor} from './angular/property_write_html_visitor'; export interface TemplateVariableAssignment { @@ -24,22 +26,20 @@ export interface TemplateVariableAssignment { */ export function analyzeResolvedTemplate( filePath: string, template: ResolvedTemplate): TemplateVariableAssignment[]|null { - try { - const templateNodes = parseTemplate(template.content, filePath).nodes; - const visitor = new PropertyWriteHtmlVisitor(); + const templateNodes = parseHtmlGracefully(template.content, filePath); - // Analyze the Angular Render3 HTML AST and collect all property assignments and - // template variables. - visitAll(visitor, templateNodes); - - return filterTemplateVariableAssignments(visitor.propertyAssignments, visitor.templateVariables) - .map(({node, start, end}) => ({node, start: start + node.span.start, end})); - } catch { - // Do nothing if the template couldn't be parsed. We don't want to throw any - // exception if a template is syntactically not valid. e.g. template could be - // using preprocessor syntax. + if (!templateNodes) { return null; } + + const visitor = new PropertyWriteHtmlVisitor(); + + // Analyze the Angular Render3 HTML AST and collect all property assignments and + // template variables. + visitAll(visitor, templateNodes); + + return filterTemplateVariableAssignments(visitor.propertyAssignments, visitor.templateVariables) + .map(({node, start, end}) => ({node, start: start + node.span.start, end})); } /** diff --git a/packages/core/schematics/migrations/template-var-assignment/google3/noTemplateVariableAssignmentRule.ts b/packages/core/schematics/migrations/template-var-assignment/google3/noTemplateVariableAssignmentRule.ts index 54aa2c53f0..34adc22a0d 100644 --- a/packages/core/schematics/migrations/template-var-assignment/google3/noTemplateVariableAssignmentRule.ts +++ b/packages/core/schematics/migrations/template-var-assignment/google3/noTemplateVariableAssignmentRule.ts @@ -9,9 +9,10 @@ import {RuleFailure, Rules} from 'tslint'; import * as ts from 'typescript'; -import {createHtmlSourceFile} from '../../../utils/tslint/tslint_html_source_file'; -import {analyzeResolvedTemplate} from '../analyze_template'; import {NgComponentTemplateVisitor} from '../../../utils/ng_component_template'; +import {createHtmlSourceFile} from '../../../utils/tslint/tslint_html_source_file'; +import {visitAllNodes} from '../../../utils/typescript/visit_nodes'; +import {analyzeResolvedTemplate} from '../analyze_template'; const FAILURE_MESSAGE = 'Found assignment to template variable. This does not work with Ivy and ' + 'needs to be updated.'; @@ -26,7 +27,7 @@ export class Rule extends Rules.TypedRule { const failures: RuleFailure[] = []; // Analyze the current source files by detecting all referenced HTML templates. - templateVisitor.visitNode(sourceFile); + visitAllNodes(sourceFile, [templateVisitor]); const {resolvedTemplates} = templateVisitor; diff --git a/packages/core/schematics/migrations/template-var-assignment/index.ts b/packages/core/schematics/migrations/template-var-assignment/index.ts index 3bb9f2d81c..43502a4c6f 100644 --- a/packages/core/schematics/migrations/template-var-assignment/index.ts +++ b/packages/core/schematics/migrations/template-var-assignment/index.ts @@ -11,11 +11,12 @@ import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit import {dirname, relative} from 'path'; import * as ts from 'typescript'; +import {NgComponentTemplateVisitor} from '../../utils/ng_component_template'; import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig'; +import {visitAllNodes} from '../../utils/typescript/visit_nodes'; import {analyzeResolvedTemplate} from './analyze_template'; -import {NgComponentTemplateVisitor} from '../../utils/ng_component_template'; type Logger = logging.LoggerApi; @@ -63,7 +64,7 @@ function runTemplateVariableAssignmentCheck( const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !); // Analyze source files by detecting HTML templates. - rootSourceFiles.forEach(sourceFile => templateVisitor.visitNode(sourceFile)); + rootSourceFiles.forEach(sourceFile => visitAllNodes(sourceFile, [templateVisitor])); const {resolvedTemplates} = templateVisitor; const collectedFailures: string[] = []; diff --git a/packages/core/schematics/test/google3/explicit_query_timing_rule_spec.ts b/packages/core/schematics/test/google3/explicit_query_timing_rule_spec.ts index 16f1e0fa06..24db264ce2 100644 --- a/packages/core/schematics/test/google3/explicit_query_timing_rule_spec.ts +++ b/packages/core/schematics/test/google3/explicit_query_timing_rule_spec.ts @@ -128,4 +128,24 @@ describe('Google3 explicitQueryTiming TSLint rule', () => { expect(failures.length).toBe(1); expect(failures[0].getFailure()).toMatch(/analysis of the query.*"{static: false}"/); }); + + it('should detect query usage in component template', () => { + writeFile('index.ts', ` + import {Component, ViewChild} from '@angular/core'; + + @Component({ + template: \` + + + \` + }) + export class MyComp { + @ViewChild('test') query: any; + } + `); + + runTSLint(); + + expectFileToContain('index.ts', `@ViewChild('test', { static: true }) query: any;`); + }); }); diff --git a/packages/core/schematics/test/static_queries_migration_spec.ts b/packages/core/schematics/test/static_queries_migration_spec.ts index ca54b88864..bbc12f92c4 100644 --- a/packages/core/schematics/test/static_queries_migration_spec.ts +++ b/packages/core/schematics/test/static_queries_migration_spec.ts @@ -1189,6 +1189,144 @@ describe('static-queries migration', () => { .toContain(`@${queryType}('test', { static: true }) query: any;`); }); + it('should detect query usage within component template', () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + + @Component({templateUrl: 'my-template.html'}) + export class MyComponent { + @${queryType}('test') query: any; + } + `); + + writeFile(`/my-template.html`, ` + + + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: true }) query: any;`); + }); + + it('should detect query usage with nested property read within component template', () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + + @Component({templateUrl: 'my-template.html'}) + export class MyComponent { + @${queryType}('test') query: any; + } + `); + + writeFile(`/my-template.html`, ` + + + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: true }) query: any;`); + }); + + it('should not mark query as static if template has template reference with same name', () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + + @Component({templateUrl: 'my-template.html'}) + export class MyComponent { + @${queryType}('test') query: any; + } + `); + + writeFile(`/my-template.html`, ` + + + + + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: false }) query: any;`); + }); + + it('should not mark query as static if template has property read with query name but different receiver', + () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + + @Component({templateUrl: 'my-template.html'}) + export class MyComponent { + myObject: {someProp: any}; + @${queryType}('test') someProp: any; + } + `); + + // This test ensures that we don't accidentally treat template property reads + // which do not refer to the query of the component instance, but have the same + // "render3Ast.PropertyRead" name, as references to the query declaration. + writeFile(`/my-template.html`, ` + + + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: false }) someProp: any;`); + }); + + it('should ignore queries accessed within element', () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + + @Component({templateUrl: 'my-template.html'}) + export class MyComponent { + @${queryType}('test') query: any; + } + `); + + writeFile(`/my-template.html`, ` + + + + + + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: false }) query: any;`); + }); + + it('should detect inherited queries used in templates', () => { + writeFile('/index.ts', ` + import {Component, ${queryType}} from '@angular/core'; + + export class ParentClass { + @${queryType}('test') query: any; + } + + @Component({templateUrl: 'my-template.html'}) + export class MyComponent extends ParentClass {} + `); + + writeFile(`/my-template.html`, ` + + + `); + + runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@${queryType}('test', { static: true }) query: any;`); + }); + it('should properly handle multiple tsconfig files', () => { writeFile('/src/index.ts', ` import {Component, ${queryType}} from '@angular/core'; diff --git a/packages/core/schematics/utils/BUILD.bazel b/packages/core/schematics/utils/BUILD.bazel index 02482f4f02..44b3e073e8 100644 --- a/packages/core/schematics/utils/BUILD.bazel +++ b/packages/core/schematics/utils/BUILD.bazel @@ -6,6 +6,7 @@ ts_library( tsconfig = "//packages/core/schematics:tsconfig.json", visibility = ["//packages/core/schematics:__subpackages__"], deps = [ + "//packages/compiler", "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", ], diff --git a/packages/core/schematics/utils/ng_component_template.ts b/packages/core/schematics/utils/ng_component_template.ts index 7c070f76ff..aac6620fde 100644 --- a/packages/core/schematics/utils/ng_component_template.ts +++ b/packages/core/schematics/utils/ng_component_template.ts @@ -16,12 +16,16 @@ import {unwrapExpression} from './typescript/functions'; import {getPropertyNameText} from './typescript/property_name'; export interface ResolvedTemplate { + /** Class declaration that contains this template. */ + container: ts.ClassDeclaration; /** File content of the given template. */ content: string; /** Start offset of the template content (e.g. in the inline source file) */ start: number; /** Whether the given template is inline or not. */ inline: boolean; + /** Path to the file that contains this template. */ + filePath: string; /** * Gets the character and line of a given position index in the template. * If the template is declared inline within a TypeScript source file, the line and @@ -40,13 +44,9 @@ export class NgComponentTemplateVisitor { constructor(public typeChecker: ts.TypeChecker) {} visitNode(node: ts.Node) { - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - this.visitClassDeclaration(node as ts.ClassDeclaration); - break; + if (node.kind === ts.SyntaxKind.ClassDeclaration) { + this.visitClassDeclaration(node as ts.ClassDeclaration); } - - ts.forEachChild(node, node => this.visitNode(node)); } private visitClassDeclaration(node: ts.ClassDeclaration) { @@ -94,7 +94,10 @@ export class NgComponentTemplateVisitor { // Need to add an offset of one to the start because the template quotes are // not part of the template content. const templateStartIdx = property.initializer.getStart() + 1; - this.resolvedTemplates.set(resolve(sourceFileName), { + const filePath = resolve(sourceFileName); + this.resolvedTemplates.set(filePath, { + filePath: filePath, + container: node, content: property.initializer.text, inline: true, start: templateStartIdx, @@ -115,6 +118,8 @@ export class NgComponentTemplateVisitor { const lineStartsMap = computeLineStartsMap(fileContent); this.resolvedTemplates.set(templatePath, { + filePath: templatePath, + container: node, content: fileContent, inline: false, start: 0, diff --git a/packages/core/schematics/utils/parse_html.ts b/packages/core/schematics/utils/parse_html.ts new file mode 100644 index 0000000000..20ec6d35bd --- /dev/null +++ b/packages/core/schematics/utils/parse_html.ts @@ -0,0 +1,25 @@ +/** + * @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 {parseTemplate} from '@angular/compiler'; +import {Node} from '@angular/compiler/src/render3/r3_ast'; + +/** + * Parses the given HTML content using the Angular compiler. In case the parsing + * fails, null is being returned. + */ +export function parseHtmlGracefully(htmlContent: string, filePath: string): Node[]|null { + try { + return parseTemplate(htmlContent, filePath).nodes; + } catch { + // Do nothing if the template couldn't be parsed. We don't want to throw any + // exception if a template is syntactically not valid. e.g. template could be + // using preprocessor syntax. + return null; + } +} diff --git a/packages/core/schematics/utils/typescript/visit_nodes.ts b/packages/core/schematics/utils/typescript/visit_nodes.ts new file mode 100644 index 0000000000..d6f9dbabda --- /dev/null +++ b/packages/core/schematics/utils/typescript/visit_nodes.ts @@ -0,0 +1,16 @@ +/** + * @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 * as ts from 'typescript'; + +export interface TypeScriptVisitor { visitNode(node: ts.Node); } + +export function visitAllNodes(node: ts.Node, visitors: TypeScriptVisitor[]) { + visitors.forEach(v => v.visitNode(node)); + ts.forEachChild(node, node => visitAllNodes(node, visitors)); +}