From 6d111546527812ab48f48e156f09ef41cb9be1ca Mon Sep 17 00:00:00 2001 From: Keen Yee Liau Date: Wed, 21 Aug 2019 14:36:00 -0700 Subject: [PATCH] refactor(language-service): Remove redudant 'TemplateInfo' type (#32250) The TemplateInfo type is an extension of AstResult, but it is not necessary at all. Instead, improve the current interface for AstResult by removing all optional fileds and include the TemplateSource in AstResult instead. PR Close #32250 --- packages/language-service/src/common.ts | 22 +-- packages/language-service/src/completions.ts | 133 +++++++++--------- packages/language-service/src/definitions.ts | 14 +- packages/language-service/src/diagnostics.ts | 37 ++--- packages/language-service/src/hover.ts | 12 +- .../language-service/src/language_service.ts | 20 ++- .../language-service/src/locate_symbol.ts | 31 ++-- packages/language-service/src/template.ts | 5 +- packages/language-service/src/types.ts | 6 +- .../language-service/src/typescript_host.ts | 129 ++++++++++------- packages/language-service/src/utils.ts | 8 +- 11 files changed, 223 insertions(+), 194 deletions(-) diff --git a/packages/language-service/src/common.ts b/packages/language-service/src/common.ts index ba578b647a..09f0f1d9d8 100644 --- a/packages/language-service/src/common.ts +++ b/packages/language-service/src/common.ts @@ -11,26 +11,14 @@ import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, C import {Diagnostic, TemplateSource} from './types'; export interface AstResult { - htmlAst?: HtmlAst[]; - templateAst?: TemplateAst[]; - directive?: CompileDirectiveMetadata; - directives?: CompileDirectiveSummary[]; - pipes?: CompilePipeSummary[]; - parseErrors?: ParseError[]; - expressionParser?: Parser; - errors?: Diagnostic[]; -} - -export interface TemplateInfo { - position?: number; - fileName?: string; - template: TemplateSource; htmlAst: HtmlAst[]; + templateAst: TemplateAst[]; directive: CompileDirectiveMetadata; directives: CompileDirectiveSummary[]; pipes: CompilePipeSummary[]; - templateAst: TemplateAst[]; + parseErrors?: ParseError[]; expressionParser: Parser; + template: TemplateSource; } export interface AttrInfo { @@ -45,3 +33,7 @@ export type SelectorInfo = { selectors: CssSelector[], map: Map }; + +export function isAstResult(result: AstResult | Diagnostic): result is AstResult { + return result.hasOwnProperty('templateAst'); +} diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index ff983438ca..de12e06c9f 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -9,7 +9,7 @@ import {AST, AstPath, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CssSelector, Element, ElementAst, ImplicitReceiver, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ParseSpan, PropertyRead, SelectorMatcher, TagContentType, Text, findNode, getHtmlTagDefinition, splitNsName} from '@angular/compiler'; import {getExpressionScope} from '@angular/compiler-cli/src/language_services'; -import {AttrInfo, TemplateInfo} from './common'; +import {AstResult, AttrInfo} from './common'; import {getExpressionCompletions} from './expressions'; import {attributeNames, elementNames, eventNames, propertyNames} from './html_info'; import {Completion, Completions, Span, Symbol, SymbolTable, TemplateSource} from './types'; @@ -28,76 +28,75 @@ const hiddenHtmlElements = { link: true, }; -export function getTemplateCompletions(templateInfo: TemplateInfo): Completions|undefined { +export function getTemplateCompletions(templateInfo: AstResult, position: number): Completions| + undefined { let result: Completions|undefined = undefined; - let {htmlAst, templateAst, template} = templateInfo; + let {htmlAst, template} = templateInfo; // The templateNode starts at the delimiter character so we add 1 to skip it. - if (templateInfo.position != null) { - let templatePosition = templateInfo.position - template.span.start; - let path = findNode(htmlAst, templatePosition); - let mostSpecific = path.tail; - if (path.empty || !mostSpecific) { - result = elementCompletions(templateInfo, path); - } else { - let astPosition = templatePosition - mostSpecific.sourceSpan.start.offset; - mostSpecific.visit( - { - visitElement(ast) { - let startTagSpan = spanOf(ast.sourceSpan); - let tagLen = ast.name.length; - if (templatePosition <= - startTagSpan.start + tagLen + 1 /* 1 for the opening angle bracked */) { - // If we are in the tag then return the element completions. - result = elementCompletions(templateInfo, path); - } else if (templatePosition < startTagSpan.end) { - // We are in the attribute section of the element (but not in an attribute). - // Return the attribute completions. - result = attributeCompletions(templateInfo, path); - } - }, - visitAttribute(ast) { - if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) { - // We are in the name of an attribute. Show attribute completions. - result = attributeCompletions(templateInfo, path); - } else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) { - result = attributeValueCompletions(templateInfo, templatePosition, ast); - } - }, - visitText(ast) { - // Check if we are in a entity. - result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition); - if (result) return result; - result = interpolationCompletions(templateInfo, templatePosition); - if (result) return result; - let element = path.first(Element); - if (element) { - let definition = getHtmlTagDefinition(element.name); - if (definition.contentType === TagContentType.PARSABLE_DATA) { - result = voidElementAttributeCompletions(templateInfo, path); - if (!result) { - // If the element can hold content Show element completions. - result = elementCompletions(templateInfo, path); - } - } - } else { - // If no element container, implies parsable data so show elements. + let templatePosition = position - template.span.start; + let path = findNode(htmlAst, templatePosition); + let mostSpecific = path.tail; + if (path.empty || !mostSpecific) { + result = elementCompletions(templateInfo, path); + } else { + let astPosition = templatePosition - mostSpecific.sourceSpan.start.offset; + mostSpecific.visit( + { + visitElement(ast) { + let startTagSpan = spanOf(ast.sourceSpan); + let tagLen = ast.name.length; + if (templatePosition <= + startTagSpan.start + tagLen + 1 /* 1 for the opening angle bracket */) { + // If we are in the tag then return the element completions. + result = elementCompletions(templateInfo, path); + } else if (templatePosition < startTagSpan.end) { + // We are in the attribute section of the element (but not in an attribute). + // Return the attribute completions. + result = attributeCompletions(templateInfo, path); + } + }, + visitAttribute(ast) { + if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) { + // We are in the name of an attribute. Show attribute completions. + result = attributeCompletions(templateInfo, path); + } else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) { + result = attributeValueCompletions(templateInfo, templatePosition, ast); + } + }, + visitText(ast) { + // Check if we are in a entity. + result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition); + if (result) return result; + result = interpolationCompletions(templateInfo, templatePosition); + if (result) return result; + let element = path.first(Element); + if (element) { + let definition = getHtmlTagDefinition(element.name); + if (definition.contentType === TagContentType.PARSABLE_DATA) { result = voidElementAttributeCompletions(templateInfo, path); if (!result) { + // If the element can hold content, show element completions. result = elementCompletions(templateInfo, path); } } - }, - visitComment(ast) {}, - visitExpansion(ast) {}, - visitExpansionCase(ast) {} + } else { + // If no element container, implies parsable data so show elements. + result = voidElementAttributeCompletions(templateInfo, path); + if (!result) { + result = elementCompletions(templateInfo, path); + } + } }, - null); - } + visitComment(ast) {}, + visitExpansion(ast) {}, + visitExpansionCase(ast) {} + }, + null); } return result; } -function attributeCompletions(info: TemplateInfo, path: AstPath): Completions|undefined { +function attributeCompletions(info: AstResult, path: AstPath): Completions|undefined { let item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail); if (item instanceof Element) { return attributeCompletionsForElement(info, item.name, item); @@ -106,7 +105,7 @@ function attributeCompletions(info: TemplateInfo, path: AstPath): Compl } function attributeCompletionsForElement( - info: TemplateInfo, elementName: string, element?: Element): Completions { + info: AstResult, elementName: string, element?: Element): Completions { const attributes = getAttributeInfosForElement(info, elementName, element); // Map all the attributes to a completion @@ -118,7 +117,7 @@ function attributeCompletionsForElement( } function getAttributeInfosForElement( - info: TemplateInfo, elementName: string, element?: Element): AttrInfo[] { + info: AstResult, elementName: string, element?: Element): AttrInfo[] { let attributes: AttrInfo[] = []; // Add html attributes @@ -189,8 +188,8 @@ function getAttributeInfosForElement( return attributes; } -function attributeValueCompletions( - info: TemplateInfo, position: number, attr: Attribute): Completions|undefined { +function attributeValueCompletions(info: AstResult, position: number, attr: Attribute): Completions| + undefined { const path = findTemplateAstAt(info.templateAst, position); const mostSpecific = path.tail; const dinfo = diagnosticInfoFromTemplateInfo(info); @@ -212,7 +211,7 @@ function attributeValueCompletions( } } -function elementCompletions(info: TemplateInfo, path: AstPath): Completions|undefined { +function elementCompletions(info: AstResult, path: AstPath): Completions|undefined { let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements)); // Collect the elements referenced by the selectors @@ -244,7 +243,7 @@ function entityCompletions(value: string, position: number): Completions|undefin return result; } -function interpolationCompletions(info: TemplateInfo, position: number): Completions|undefined { +function interpolationCompletions(info: AstResult, position: number): Completions|undefined { // Look for an interpolation in at the position. const templatePath = findTemplateAstAt(info.templateAst, position); const mostSpecific = templatePath.tail; @@ -263,7 +262,7 @@ function interpolationCompletions(info: TemplateInfo, position: number): Complet // the attributes of an "a" element, not requesting completion in the a text element. This // code checks for this case and returns element completions if it is detected or undefined // if it is not. -function voidElementAttributeCompletions(info: TemplateInfo, path: AstPath): Completions| +function voidElementAttributeCompletions(info: AstResult, path: AstPath): Completions| undefined { let tail = path.tail; if (tail instanceof Text) { @@ -282,7 +281,7 @@ class ExpressionVisitor extends NullTemplateVisitor { result: Completion[]|undefined; constructor( - private info: TemplateInfo, private position: number, private attr?: Attribute, + private info: AstResult, private position: number, private attr?: Attribute, getExpressionScope?: () => SymbolTable) { super(); this.getExpressionScope = getExpressionScope || (() => info.template.members); diff --git a/packages/language-service/src/definitions.ts b/packages/language-service/src/definitions.ts index 297861d6f8..fe8c5dffa2 100644 --- a/packages/language-service/src/definitions.ts +++ b/packages/language-service/src/definitions.ts @@ -7,7 +7,7 @@ */ import * as ts from 'typescript'; // used as value and is provided at runtime -import {TemplateInfo} from './common'; +import {AstResult} from './common'; import {locateSymbol} from './locate_symbol'; import {Span} from './types'; @@ -23,9 +23,15 @@ function ngSpanToTsTextSpan(span: Span): ts.TextSpan { }; } -export function getDefinitionAndBoundSpan(info: TemplateInfo): ts.DefinitionInfoAndBoundSpan| - undefined { - const symbolInfo = locateSymbol(info); +/** + * Traverse the template AST and look for the symbol located at `position`, then + * return its definition and span of bound text. + * @param info + * @param position + */ +export function getDefinitionAndBoundSpan( + info: AstResult, position: number): ts.DefinitionInfoAndBoundSpan|undefined { + const symbolInfo = locateSymbol(info, position); if (!symbolInfo) { return; } diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index fffaa88645..90850f0a4a 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -16,41 +16,28 @@ import {offsetSpan, spanOf} from './utils'; /** * Return diagnostic information for the parsed AST of the template. - * @param template source of the template and class information * @param ast contains HTML and template AST */ -export function getTemplateDiagnostics( - template: ng.TemplateSource, ast: AstResult): ng.Diagnostic[] { +export function getTemplateDiagnostics(ast: AstResult): ng.Diagnostic[] { const results: ng.Diagnostic[] = []; - - if (ast.parseErrors && ast.parseErrors.length) { - results.push(...ast.parseErrors.map(e => { + const {parseErrors, templateAst, htmlAst, template} = ast; + if (parseErrors) { + results.push(...parseErrors.map(e => { return { kind: ng.DiagnosticKind.Error, span: offsetSpan(spanOf(e.span), template.span.start), message: e.msg, }; })); - } else if (ast.templateAst && ast.htmlAst) { - const expressionDiagnostics = getTemplateExpressionDiagnostics({ - templateAst: ast.templateAst, - htmlAst: ast.htmlAst, - offset: template.span.start, - query: template.query, - members: template.members, - }); - results.push(...expressionDiagnostics); } - if (ast.errors) { - results.push(...ast.errors.map(e => { - return { - kind: e.kind, - span: e.span || template.span, - message: e.message, - }; - })); - } - + const expressionDiagnostics = getTemplateExpressionDiagnostics({ + templateAst: templateAst, + htmlAst: htmlAst, + offset: template.span.start, + query: template.query, + members: template.members, + }); + results.push(...expressionDiagnostics); return results; } diff --git a/packages/language-service/src/hover.ts b/packages/language-service/src/hover.ts index d1f6f50990..79543a21de 100644 --- a/packages/language-service/src/hover.ts +++ b/packages/language-service/src/hover.ts @@ -7,15 +7,21 @@ */ import * as ts from 'typescript'; -import {TemplateInfo} from './common'; +import {AstResult} from './common'; import {locateSymbol} from './locate_symbol'; // Reverse mappings of enum would generate strings const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; -export function getHover(info: TemplateInfo): ts.QuickInfo|undefined { - const symbolInfo = locateSymbol(info); +/** + * Traverse the template AST and look for the symbol located at `position`, then + * return the corresponding quick info. + * @param info template AST + * @param position location of the symbol + */ +export function getHover(info: AstResult, position: number): ts.QuickInfo|undefined { + const symbolInfo = locateSymbol(info, position); if (!symbolInfo) { return; } diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index f3c5d2b02e..7fa9b4942c 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -7,6 +7,8 @@ */ import * as tss from 'typescript/lib/tsserverlibrary'; + +import {isAstResult} from './common'; import {getTemplateCompletions, ngCompletionToTsCompletionEntry} from './completions'; import {getDefinitionAndBoundSpan} from './definitions'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics'; @@ -36,8 +38,12 @@ class LanguageServiceImpl implements LanguageService { const results: Diagnostic[] = []; const templates = this.host.getTemplates(fileName); for (const template of templates) { - const ast = this.host.getTemplateAst(template); - results.push(...getTemplateDiagnostics(template, ast)); + const astOrDiagnostic = this.host.getTemplateAst(template); + if (isAstResult(astOrDiagnostic)) { + results.push(...getTemplateDiagnostics(astOrDiagnostic)); + } else { + results.push(astOrDiagnostic); + } } const declarations = this.host.getDeclarations(fileName); if (declarations && declarations.length) { @@ -52,11 +58,11 @@ class LanguageServiceImpl implements LanguageService { getCompletionsAt(fileName: string, position: number): tss.CompletionInfo|undefined { this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' - const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); - if (!templateInfo) { + const ast = this.host.getTemplateAstAtPosition(fileName, position); + if (!ast) { return; } - const results = getTemplateCompletions(templateInfo); + const results = getTemplateCompletions(ast, position); if (!results || !results.length) { return; } @@ -72,7 +78,7 @@ class LanguageServiceImpl implements LanguageService { this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { - return getDefinitionAndBoundSpan(templateInfo); + return getDefinitionAndBoundSpan(templateInfo, position); } } @@ -80,7 +86,7 @@ class LanguageServiceImpl implements LanguageService { this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { - return getHover(templateInfo); + return getHover(templateInfo, position); } } } diff --git a/packages/language-service/src/locate_symbol.ts b/packages/language-service/src/locate_symbol.ts index fd07d095d2..01f27a25ac 100644 --- a/packages/language-service/src/locate_symbol.ts +++ b/packages/language-service/src/locate_symbol.ts @@ -9,7 +9,7 @@ import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAstPath, findNode, tokenReference} from '@angular/compiler'; import {getExpressionScope} from '@angular/compiler-cli/src/language_services'; -import {TemplateInfo} from './common'; +import {AstResult} from './common'; import {getExpressionSymbol} from './expressions'; import {Definition, Span, Symbol} from './types'; import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, inSpan, offsetSpan, spanOf} from './utils'; @@ -19,15 +19,19 @@ export interface SymbolInfo { span: Span; } -export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { - if (!info.position) return undefined; - const templatePosition = info.position - info.template.span.start; +/** + * Traverse the template AST and locate the Symbol at the specified `position`. + * @param info Ast and Template Source + * @param position location to look for + */ +export function locateSymbol(info: AstResult, position: number): SymbolInfo|undefined { + const templatePosition = position - info.template.span.start; const path = findTemplateAstAt(info.templateAst, templatePosition); if (path.tail) { let symbol: Symbol|undefined = undefined; let span: Span|undefined = undefined; const attributeValueSymbol = (ast: AST, inEvent: boolean = false): boolean => { - const attribute = findAttribute(info); + const attribute = findAttribute(info, position); if (attribute) { if (inSpan(templatePosition, spanOf(attribute.valueSpan))) { const dinfo = diagnosticInfoFromTemplateInfo(info); @@ -113,17 +117,14 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { } } -function findAttribute(info: TemplateInfo): Attribute|undefined { - if (info.position) { - const templatePosition = info.position - info.template.span.start; - const path = findNode(info.htmlAst, templatePosition); - return path.first(Attribute); - } +function findAttribute(info: AstResult, position: number): Attribute|undefined { + const templatePosition = position - info.template.span.start; + const path = findNode(info.htmlAst, templatePosition); + return path.first(Attribute); } function findInputBinding( - info: TemplateInfo, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol| - undefined { + info: AstResult, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol|undefined { const element = path.first(ElementAst); if (element) { for (const directive of element.directives) { @@ -139,8 +140,8 @@ function findInputBinding( } } -function findOutputBinding( - info: TemplateInfo, path: TemplateAstPath, binding: BoundEventAst): Symbol|undefined { +function findOutputBinding(info: AstResult, path: TemplateAstPath, binding: BoundEventAst): Symbol| + undefined { const element = path.first(ElementAst); if (element) { for (const directive of element.directives) { diff --git a/packages/language-service/src/template.ts b/packages/language-service/src/template.ts index 241746bcc4..904d146ff4 100644 --- a/packages/language-service/src/template.ts +++ b/packages/language-service/src/template.ts @@ -8,6 +8,8 @@ import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli'; import * as ts from 'typescript'; + +import {isAstResult} from './common'; import * as ng from './types'; import {TypeScriptServiceHost} from './typescript_host'; @@ -67,7 +69,8 @@ abstract class BaseTemplate implements ng.TemplateSource { // TODO: There is circular dependency here between TemplateSource and // TypeScriptHost. Consider refactoring the code to break this cycle. const ast = this.host.getTemplateAst(this); - return getPipesTable(sourceFile, program, typeChecker, ast.pipes || []); + const pipes = isAstResult(ast) ? ast.pipes : []; + return getPipesTable(sourceFile, program, typeChecker, pipes); }); } return this.queryCache; diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index 864492f888..46ba469002 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -9,7 +9,7 @@ import {CompileDirectiveMetadata, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from '@angular/compiler-cli/src/language_services'; -import {AstResult, TemplateInfo} from './common'; +import {AstResult} from './common'; export { BuiltinType, @@ -192,12 +192,12 @@ export interface LanguageServiceHost { /** * Return the AST for both HTML and template for the contextFile. */ - getTemplateAst(template: TemplateSource): AstResult; + getTemplateAst(template: TemplateSource): AstResult|Diagnostic; /** * Return the template AST for the node that corresponds to the position. */ - getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined; + getTemplateAstAtPosition(fileName: string, position: number): AstResult|undefined; } /** diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index ee1bce4b29..d6c968c9c7 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -6,17 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotSummaryResolver, CompileMetadataResolver, CompileNgModuleMetadata, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, I18NHtmlParser, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler'; -import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; +import {AotSummaryResolver, CompileDirectiveSummary, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, I18NHtmlParser, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler'; +import {SchemaMetadata, ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as ts from 'typescript'; -import {AstResult, TemplateInfo} from './common'; +import {AstResult, isAstResult} from './common'; import {createLanguageService} from './language_service'; import {ReflectorHost} from './reflector_host'; import {ExternalTemplate, InlineTemplate, getClassDeclFromTemplateNode} from './template'; import {Declaration, DeclarationError, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; import {findTightestNode, getDirectiveClassLike} from './utils'; + /** * Create a `LanguageServiceHost` */ @@ -385,7 +386,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost { * @param fileName TS or HTML file * @param position Position of the template in the TS file, otherwise ignored. */ - getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined { + getTemplateAstAtPosition(fileName: string, position: number): AstResult|undefined { let template: TemplateSource|undefined; if (fileName.endsWith('.ts')) { const sourceFile = this.getSourceFile(fileName); @@ -405,66 +406,94 @@ export class TypeScriptServiceHost implements LanguageServiceHost { return; } const astResult = this.getTemplateAst(template); - if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive && - astResult.directives && astResult.pipes && astResult.expressionParser) { - return { - position, - fileName, - template, - htmlAst: astResult.htmlAst, - directive: astResult.directive, - directives: astResult.directives, - pipes: astResult.pipes, - templateAst: astResult.templateAst, - expressionParser: astResult.expressionParser - }; + if (!isAstResult(astResult)) { + return; } + return astResult; } - getTemplateAst(template: TemplateSource): AstResult { + /** + * Find the NgModule which the directive associated with the `classSymbol` + * belongs to, then return its schema and transitive directives and pipes. + * @param classSymbol Angular Symbol that defines a directive + */ + private getModuleMetadataForDirective(classSymbol: StaticSymbol) { + const result = { + directives: [] as CompileDirectiveSummary[], + pipes: [] as CompilePipeSummary[], + schemas: [] as SchemaMetadata[], + }; + // First find which NgModule the directive belongs to. + const ngModule = this.analyzedModules.ngModuleByPipeOrDirective.get(classSymbol) || + findSuitableDefaultModule(this.analyzedModules); + if (!ngModule) { + return result; + } + // Then gather all transitive directives and pipes. + const {directives, pipes} = ngModule.transitiveModule; + for (const directive of directives) { + const data = this.resolver.getNonNormalizedDirectiveMetadata(directive.reference); + if (data) { + result.directives.push(data.metadata.toSummary()); + } + } + for (const pipe of pipes) { + const metadata = this.resolver.getOrLoadPipeMetadata(pipe.reference); + result.pipes.push(metadata.toSummary()); + } + result.schemas.push(...ngModule.schemas); + return result; + } + + /** + * Parse the `template` and return its AST if there's no error. Otherwise + * return a Diagnostic message. + * @param template template to be parsed + */ + getTemplateAst(template: TemplateSource): AstResult|Diagnostic { + const {type: classSymbol, fileName} = template; try { - const resolvedMetadata = this.resolver.getNonNormalizedDirectiveMetadata(template.type); - const metadata = resolvedMetadata && resolvedMetadata.metadata; - if (!metadata) { - return {}; + const data = this.resolver.getNonNormalizedDirectiveMetadata(classSymbol); + if (!data) { + return { + kind: DiagnosticKind.Error, + message: `No metadata found for '${classSymbol.name}' in ${fileName}.`, + span: template.span, + }; } - const rawHtmlParser = new HtmlParser(); - const htmlParser = new I18NHtmlParser(rawHtmlParser); + const htmlParser = new I18NHtmlParser(new HtmlParser()); const expressionParser = new Parser(new Lexer()); - const config = new CompilerConfig(); const parser = new TemplateParser( - config, this.reflector, expressionParser, new DomElementSchemaRegistry(), htmlParser, - null !, []); - const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true}); - const errors: Diagnostic[]|undefined = undefined; - const ngModule = this.analyzedModules.ngModuleByPipeOrDirective.get(template.type) || - // Reported by the the declaration diagnostics. - findSuitableDefaultModule(this.analyzedModules); - if (!ngModule) { - return {}; + new CompilerConfig(), this.reflector, expressionParser, new DomElementSchemaRegistry(), + htmlParser, + null !, // console + [] // tranforms + ); + const htmlResult = htmlParser.parse(template.source, fileName, { + tokenizeExpansionForms: true, + }); + const {directives, pipes, schemas} = this.getModuleMetadataForDirective(classSymbol); + const parseResult = + parser.tryParseHtml(htmlResult, data.metadata, directives, pipes, schemas); + if (!parseResult.templateAst) { + return { + kind: DiagnosticKind.Error, + message: `Failed to parse template for '${classSymbol.name}' in ${fileName}`, + span: template.span, + }; } - const directives = ngModule.transitiveModule.directives - .map(d => this.resolver.getNonNormalizedDirectiveMetadata(d.reference)) - .filter(d => d) - .map(d => d !.metadata.toSummary()); - const pipes = ngModule.transitiveModule.pipes.map( - p => this.resolver.getOrLoadPipeMetadata(p.reference).toSummary()); - const schemas = ngModule.schemas; - const parseResult = parser.tryParseHtml(htmlResult, metadata, directives, pipes, schemas); return { htmlAst: htmlResult.rootNodes, templateAst: parseResult.templateAst, - directive: metadata, directives, pipes, - parseErrors: parseResult.errors, expressionParser, errors + directive: data.metadata, directives, pipes, + parseErrors: parseResult.errors, expressionParser, template, }; } catch (e) { - const span = e.fileName === template.fileName && template.query.getSpanAt(e.line, e.column) || - template.span; return { - errors: [{ - kind: DiagnosticKind.Error, - message: e.message, span, - }], + kind: DiagnosticKind.Error, + message: e.message, + span: + e.fileName === fileName && template.query.getSpanAt(e.line, e.column) || template.span, }; } } diff --git a/packages/language-service/src/utils.ts b/packages/language-service/src/utils.ts index 0a116d72d4..5899f98f8c 100644 --- a/packages/language-service/src/utils.ts +++ b/packages/language-service/src/utils.ts @@ -10,7 +10,7 @@ import {AstPath, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, Dire import {DiagnosticTemplateInfo} from '@angular/compiler-cli/src/language_services'; import * as ts from 'typescript'; -import {SelectorInfo, TemplateInfo} from './common'; +import {AstResult, SelectorInfo} from './common'; import {Span} from './types'; export interface SpanHolder { @@ -67,7 +67,7 @@ export function hasTemplateReference(type: CompileTypeMetadata): boolean { return false; } -export function getSelectors(info: TemplateInfo): SelectorInfo { +export function getSelectors(info: AstResult): SelectorInfo { const map = new Map(); const selectors: CssSelector[] = flatten(info.directives.map(directive => { const selectors: CssSelector[] = CssSelector.parse(directive.selector !); @@ -113,9 +113,9 @@ export function isTypescriptVersion(low: string, high?: string) { return true; } -export function diagnosticInfoFromTemplateInfo(info: TemplateInfo): DiagnosticTemplateInfo { +export function diagnosticInfoFromTemplateInfo(info: AstResult): DiagnosticTemplateInfo { return { - fileName: info.fileName, + fileName: info.template.fileName, offset: info.template.span.start, query: info.template.query, members: info.template.members,