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,