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
This commit is contained in:
Keen Yee Liau 2019-08-21 14:36:00 -07:00 committed by atscott
parent 6b245a39ee
commit 6d11154652
11 changed files with 223 additions and 194 deletions

View File

@ -11,26 +11,14 @@ import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, C
import {Diagnostic, TemplateSource} from './types'; import {Diagnostic, TemplateSource} from './types';
export interface AstResult { 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[]; htmlAst: HtmlAst[];
templateAst: TemplateAst[];
directive: CompileDirectiveMetadata; directive: CompileDirectiveMetadata;
directives: CompileDirectiveSummary[]; directives: CompileDirectiveSummary[];
pipes: CompilePipeSummary[]; pipes: CompilePipeSummary[];
templateAst: TemplateAst[]; parseErrors?: ParseError[];
expressionParser: Parser; expressionParser: Parser;
template: TemplateSource;
} }
export interface AttrInfo { export interface AttrInfo {
@ -45,3 +33,7 @@ export type SelectorInfo = {
selectors: CssSelector[], selectors: CssSelector[],
map: Map<CssSelector, CompileDirectiveSummary> map: Map<CssSelector, CompileDirectiveSummary>
}; };
export function isAstResult(result: AstResult | Diagnostic): result is AstResult {
return result.hasOwnProperty('templateAst');
}

View File

@ -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 {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 {getExpressionScope} from '@angular/compiler-cli/src/language_services';
import {AttrInfo, TemplateInfo} from './common'; import {AstResult, AttrInfo} from './common';
import {getExpressionCompletions} from './expressions'; import {getExpressionCompletions} from './expressions';
import {attributeNames, elementNames, eventNames, propertyNames} from './html_info'; import {attributeNames, elementNames, eventNames, propertyNames} from './html_info';
import {Completion, Completions, Span, Symbol, SymbolTable, TemplateSource} from './types'; import {Completion, Completions, Span, Symbol, SymbolTable, TemplateSource} from './types';
@ -28,76 +28,75 @@ const hiddenHtmlElements = {
link: true, link: true,
}; };
export function getTemplateCompletions(templateInfo: TemplateInfo): Completions|undefined { export function getTemplateCompletions(templateInfo: AstResult, position: number): Completions|
undefined {
let result: Completions|undefined = 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. // The templateNode starts at the delimiter character so we add 1 to skip it.
if (templateInfo.position != null) { let templatePosition = position - template.span.start;
let templatePosition = templateInfo.position - template.span.start; let path = findNode(htmlAst, templatePosition);
let path = findNode(htmlAst, templatePosition); let mostSpecific = path.tail;
let mostSpecific = path.tail; if (path.empty || !mostSpecific) {
if (path.empty || !mostSpecific) { result = elementCompletions(templateInfo, path);
result = elementCompletions(templateInfo, path); } else {
} else { let astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
let astPosition = templatePosition - mostSpecific.sourceSpan.start.offset; mostSpecific.visit(
mostSpecific.visit( {
{ visitElement(ast) {
visitElement(ast) { let startTagSpan = spanOf(ast.sourceSpan);
let startTagSpan = spanOf(ast.sourceSpan); let tagLen = ast.name.length;
let tagLen = ast.name.length; if (templatePosition <=
if (templatePosition <= startTagSpan.start + tagLen + 1 /* 1 for the opening angle bracket */) {
startTagSpan.start + tagLen + 1 /* 1 for the opening angle bracked */) { // If we are in the tag then return the element completions.
// If we are in the tag then return the element completions. result = elementCompletions(templateInfo, path);
result = elementCompletions(templateInfo, path); } else if (templatePosition < startTagSpan.end) {
} else if (templatePosition < startTagSpan.end) { // We are in the attribute section of the element (but not in an attribute).
// We are in the attribute section of the element (but not in an attribute). // Return the attribute completions.
// Return the attribute completions. result = attributeCompletions(templateInfo, path);
result = attributeCompletions(templateInfo, path); }
} },
}, visitAttribute(ast) {
visitAttribute(ast) { if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) {
if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) { // We are in the name of an attribute. Show attribute completions.
// We are in the name of an attribute. Show attribute completions. result = attributeCompletions(templateInfo, path);
result = attributeCompletions(templateInfo, path); } else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
} else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) { result = attributeValueCompletions(templateInfo, templatePosition, ast);
result = attributeValueCompletions(templateInfo, templatePosition, ast); }
} },
}, visitText(ast) {
visitText(ast) { // Check if we are in a entity.
// Check if we are in a entity. result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition);
result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition); if (result) return result;
if (result) return result; result = interpolationCompletions(templateInfo, templatePosition);
result = interpolationCompletions(templateInfo, templatePosition); if (result) return result;
if (result) return result; let element = path.first(Element);
let element = path.first(Element); if (element) {
if (element) { let definition = getHtmlTagDefinition(element.name);
let definition = getHtmlTagDefinition(element.name); if (definition.contentType === TagContentType.PARSABLE_DATA) {
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.
result = voidElementAttributeCompletions(templateInfo, path); result = voidElementAttributeCompletions(templateInfo, path);
if (!result) { if (!result) {
// If the element can hold content, show element completions.
result = elementCompletions(templateInfo, path); result = elementCompletions(templateInfo, path);
} }
} }
}, } else {
visitComment(ast) {}, // If no element container, implies parsable data so show elements.
visitExpansion(ast) {}, result = voidElementAttributeCompletions(templateInfo, path);
visitExpansionCase(ast) {} if (!result) {
result = elementCompletions(templateInfo, path);
}
}
}, },
null); visitComment(ast) {},
} visitExpansion(ast) {},
visitExpansionCase(ast) {}
},
null);
} }
return result; return result;
} }
function attributeCompletions(info: TemplateInfo, path: AstPath<HtmlAst>): Completions|undefined { function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): Completions|undefined {
let item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail); let item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
if (item instanceof Element) { if (item instanceof Element) {
return attributeCompletionsForElement(info, item.name, item); return attributeCompletionsForElement(info, item.name, item);
@ -106,7 +105,7 @@ function attributeCompletions(info: TemplateInfo, path: AstPath<HtmlAst>): Compl
} }
function attributeCompletionsForElement( function attributeCompletionsForElement(
info: TemplateInfo, elementName: string, element?: Element): Completions { info: AstResult, elementName: string, element?: Element): Completions {
const attributes = getAttributeInfosForElement(info, elementName, element); const attributes = getAttributeInfosForElement(info, elementName, element);
// Map all the attributes to a completion // Map all the attributes to a completion
@ -118,7 +117,7 @@ function attributeCompletionsForElement(
} }
function getAttributeInfosForElement( function getAttributeInfosForElement(
info: TemplateInfo, elementName: string, element?: Element): AttrInfo[] { info: AstResult, elementName: string, element?: Element): AttrInfo[] {
let attributes: AttrInfo[] = []; let attributes: AttrInfo[] = [];
// Add html attributes // Add html attributes
@ -189,8 +188,8 @@ function getAttributeInfosForElement(
return attributes; return attributes;
} }
function attributeValueCompletions( function attributeValueCompletions(info: AstResult, position: number, attr: Attribute): Completions|
info: TemplateInfo, position: number, attr: Attribute): Completions|undefined { undefined {
const path = findTemplateAstAt(info.templateAst, position); const path = findTemplateAstAt(info.templateAst, position);
const mostSpecific = path.tail; const mostSpecific = path.tail;
const dinfo = diagnosticInfoFromTemplateInfo(info); const dinfo = diagnosticInfoFromTemplateInfo(info);
@ -212,7 +211,7 @@ function attributeValueCompletions(
} }
} }
function elementCompletions(info: TemplateInfo, path: AstPath<HtmlAst>): Completions|undefined { function elementCompletions(info: AstResult, path: AstPath<HtmlAst>): Completions|undefined {
let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements)); let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements));
// Collect the elements referenced by the selectors // Collect the elements referenced by the selectors
@ -244,7 +243,7 @@ function entityCompletions(value: string, position: number): Completions|undefin
return result; 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. // Look for an interpolation in at the position.
const templatePath = findTemplateAstAt(info.templateAst, position); const templatePath = findTemplateAstAt(info.templateAst, position);
const mostSpecific = templatePath.tail; 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 // 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 // code checks for this case and returns element completions if it is detected or undefined
// if it is not. // if it is not.
function voidElementAttributeCompletions(info: TemplateInfo, path: AstPath<HtmlAst>): Completions| function voidElementAttributeCompletions(info: AstResult, path: AstPath<HtmlAst>): Completions|
undefined { undefined {
let tail = path.tail; let tail = path.tail;
if (tail instanceof Text) { if (tail instanceof Text) {
@ -282,7 +281,7 @@ class ExpressionVisitor extends NullTemplateVisitor {
result: Completion[]|undefined; result: Completion[]|undefined;
constructor( constructor(
private info: TemplateInfo, private position: number, private attr?: Attribute, private info: AstResult, private position: number, private attr?: Attribute,
getExpressionScope?: () => SymbolTable) { getExpressionScope?: () => SymbolTable) {
super(); super();
this.getExpressionScope = getExpressionScope || (() => info.template.members); this.getExpressionScope = getExpressionScope || (() => info.template.members);

View File

@ -7,7 +7,7 @@
*/ */
import * as ts from 'typescript'; // used as value and is provided at runtime 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 {locateSymbol} from './locate_symbol';
import {Span} from './types'; import {Span} from './types';
@ -23,9 +23,15 @@ function ngSpanToTsTextSpan(span: Span): ts.TextSpan {
}; };
} }
export function getDefinitionAndBoundSpan(info: TemplateInfo): ts.DefinitionInfoAndBoundSpan| /**
undefined { * Traverse the template AST and look for the symbol located at `position`, then
const symbolInfo = locateSymbol(info); * 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) { if (!symbolInfo) {
return; return;
} }

View File

@ -16,41 +16,28 @@ import {offsetSpan, spanOf} from './utils';
/** /**
* Return diagnostic information for the parsed AST of the template. * 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 * @param ast contains HTML and template AST
*/ */
export function getTemplateDiagnostics( export function getTemplateDiagnostics(ast: AstResult): ng.Diagnostic[] {
template: ng.TemplateSource, ast: AstResult): ng.Diagnostic[] {
const results: ng.Diagnostic[] = []; const results: ng.Diagnostic[] = [];
const {parseErrors, templateAst, htmlAst, template} = ast;
if (ast.parseErrors && ast.parseErrors.length) { if (parseErrors) {
results.push(...ast.parseErrors.map(e => { results.push(...parseErrors.map(e => {
return { return {
kind: ng.DiagnosticKind.Error, kind: ng.DiagnosticKind.Error,
span: offsetSpan(spanOf(e.span), template.span.start), span: offsetSpan(spanOf(e.span), template.span.start),
message: e.msg, 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) { const expressionDiagnostics = getTemplateExpressionDiagnostics({
results.push(...ast.errors.map(e => { templateAst: templateAst,
return { htmlAst: htmlAst,
kind: e.kind, offset: template.span.start,
span: e.span || template.span, query: template.query,
message: e.message, members: template.members,
}; });
})); results.push(...expressionDiagnostics);
}
return results; return results;
} }

View File

@ -7,15 +7,21 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {TemplateInfo} from './common'; import {AstResult} from './common';
import {locateSymbol} from './locate_symbol'; import {locateSymbol} from './locate_symbol';
// Reverse mappings of enum would generate strings // Reverse mappings of enum would generate strings
const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; 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) { if (!symbolInfo) {
return; return;
} }

View File

@ -7,6 +7,8 @@
*/ */
import * as tss from 'typescript/lib/tsserverlibrary'; import * as tss from 'typescript/lib/tsserverlibrary';
import {isAstResult} from './common';
import {getTemplateCompletions, ngCompletionToTsCompletionEntry} from './completions'; import {getTemplateCompletions, ngCompletionToTsCompletionEntry} from './completions';
import {getDefinitionAndBoundSpan} from './definitions'; import {getDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics';
@ -36,8 +38,12 @@ class LanguageServiceImpl implements LanguageService {
const results: Diagnostic[] = []; const results: Diagnostic[] = [];
const templates = this.host.getTemplates(fileName); const templates = this.host.getTemplates(fileName);
for (const template of templates) { for (const template of templates) {
const ast = this.host.getTemplateAst(template); const astOrDiagnostic = this.host.getTemplateAst(template);
results.push(...getTemplateDiagnostics(template, ast)); if (isAstResult(astOrDiagnostic)) {
results.push(...getTemplateDiagnostics(astOrDiagnostic));
} else {
results.push(astOrDiagnostic);
}
} }
const declarations = this.host.getDeclarations(fileName); const declarations = this.host.getDeclarations(fileName);
if (declarations && declarations.length) { if (declarations && declarations.length) {
@ -52,11 +58,11 @@ class LanguageServiceImpl implements LanguageService {
getCompletionsAt(fileName: string, position: number): tss.CompletionInfo|undefined { getCompletionsAt(fileName: string, position: number): tss.CompletionInfo|undefined {
this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); const ast = this.host.getTemplateAstAtPosition(fileName, position);
if (!templateInfo) { if (!ast) {
return; return;
} }
const results = getTemplateCompletions(templateInfo); const results = getTemplateCompletions(ast, position);
if (!results || !results.length) { if (!results || !results.length) {
return; return;
} }
@ -72,7 +78,7 @@ class LanguageServiceImpl implements LanguageService {
this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) { if (templateInfo) {
return getDefinitionAndBoundSpan(templateInfo); return getDefinitionAndBoundSpan(templateInfo, position);
} }
} }
@ -80,7 +86,7 @@ class LanguageServiceImpl implements LanguageService {
this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) { if (templateInfo) {
return getHover(templateInfo); return getHover(templateInfo, position);
} }
} }
} }

View File

@ -9,7 +9,7 @@
import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAstPath, findNode, tokenReference} from '@angular/compiler'; import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAstPath, findNode, tokenReference} from '@angular/compiler';
import {getExpressionScope} from '@angular/compiler-cli/src/language_services'; import {getExpressionScope} from '@angular/compiler-cli/src/language_services';
import {TemplateInfo} from './common'; import {AstResult} from './common';
import {getExpressionSymbol} from './expressions'; import {getExpressionSymbol} from './expressions';
import {Definition, Span, Symbol} from './types'; import {Definition, Span, Symbol} from './types';
import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, inSpan, offsetSpan, spanOf} from './utils'; import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, inSpan, offsetSpan, spanOf} from './utils';
@ -19,15 +19,19 @@ export interface SymbolInfo {
span: Span; span: Span;
} }
export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { /**
if (!info.position) return undefined; * Traverse the template AST and locate the Symbol at the specified `position`.
const templatePosition = info.position - info.template.span.start; * @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); const path = findTemplateAstAt(info.templateAst, templatePosition);
if (path.tail) { if (path.tail) {
let symbol: Symbol|undefined = undefined; let symbol: Symbol|undefined = undefined;
let span: Span|undefined = undefined; let span: Span|undefined = undefined;
const attributeValueSymbol = (ast: AST, inEvent: boolean = false): boolean => { const attributeValueSymbol = (ast: AST, inEvent: boolean = false): boolean => {
const attribute = findAttribute(info); const attribute = findAttribute(info, position);
if (attribute) { if (attribute) {
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) { if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
const dinfo = diagnosticInfoFromTemplateInfo(info); const dinfo = diagnosticInfoFromTemplateInfo(info);
@ -113,17 +117,14 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
} }
} }
function findAttribute(info: TemplateInfo): Attribute|undefined { function findAttribute(info: AstResult, position: number): Attribute|undefined {
if (info.position) { const templatePosition = position - info.template.span.start;
const templatePosition = info.position - info.template.span.start; const path = findNode(info.htmlAst, templatePosition);
const path = findNode(info.htmlAst, templatePosition); return path.first(Attribute);
return path.first(Attribute);
}
} }
function findInputBinding( function findInputBinding(
info: TemplateInfo, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol| info: AstResult, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol|undefined {
undefined {
const element = path.first(ElementAst); const element = path.first(ElementAst);
if (element) { if (element) {
for (const directive of element.directives) { for (const directive of element.directives) {
@ -139,8 +140,8 @@ function findInputBinding(
} }
} }
function findOutputBinding( function findOutputBinding(info: AstResult, path: TemplateAstPath, binding: BoundEventAst): Symbol|
info: TemplateInfo, path: TemplateAstPath, binding: BoundEventAst): Symbol|undefined { undefined {
const element = path.first(ElementAst); const element = path.first(ElementAst);
if (element) { if (element) {
for (const directive of element.directives) { for (const directive of element.directives) {

View File

@ -8,6 +8,8 @@
import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli'; import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {isAstResult} from './common';
import * as ng from './types'; import * as ng from './types';
import {TypeScriptServiceHost} from './typescript_host'; import {TypeScriptServiceHost} from './typescript_host';
@ -67,7 +69,8 @@ abstract class BaseTemplate implements ng.TemplateSource {
// TODO: There is circular dependency here between TemplateSource and // TODO: There is circular dependency here between TemplateSource and
// TypeScriptHost. Consider refactoring the code to break this cycle. // TypeScriptHost. Consider refactoring the code to break this cycle.
const ast = this.host.getTemplateAst(this); 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; return this.queryCache;

View File

@ -9,7 +9,7 @@
import {CompileDirectiveMetadata, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; 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 {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 { export {
BuiltinType, BuiltinType,
@ -192,12 +192,12 @@ export interface LanguageServiceHost {
/** /**
* Return the AST for both HTML and template for the contextFile. * 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. * 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;
} }
/** /**

View File

@ -6,17 +6,18 @@
* found in the LICENSE file at https://angular.io/license * 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 {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 {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import {SchemaMetadata, ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AstResult, TemplateInfo} from './common'; import {AstResult, isAstResult} from './common';
import {createLanguageService} from './language_service'; import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host'; import {ReflectorHost} from './reflector_host';
import {ExternalTemplate, InlineTemplate, getClassDeclFromTemplateNode} from './template'; import {ExternalTemplate, InlineTemplate, getClassDeclFromTemplateNode} from './template';
import {Declaration, DeclarationError, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; import {Declaration, DeclarationError, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
import {findTightestNode, getDirectiveClassLike} from './utils'; import {findTightestNode, getDirectiveClassLike} from './utils';
/** /**
* Create a `LanguageServiceHost` * Create a `LanguageServiceHost`
*/ */
@ -385,7 +386,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
* @param fileName TS or HTML file * @param fileName TS or HTML file
* @param position Position of the template in the TS file, otherwise ignored. * @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; let template: TemplateSource|undefined;
if (fileName.endsWith('.ts')) { if (fileName.endsWith('.ts')) {
const sourceFile = this.getSourceFile(fileName); const sourceFile = this.getSourceFile(fileName);
@ -405,66 +406,94 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return; return;
} }
const astResult = this.getTemplateAst(template); const astResult = this.getTemplateAst(template);
if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive && if (!isAstResult(astResult)) {
astResult.directives && astResult.pipes && astResult.expressionParser) { return;
return {
position,
fileName,
template,
htmlAst: astResult.htmlAst,
directive: astResult.directive,
directives: astResult.directives,
pipes: astResult.pipes,
templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser
};
} }
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 { try {
const resolvedMetadata = this.resolver.getNonNormalizedDirectiveMetadata(template.type); const data = this.resolver.getNonNormalizedDirectiveMetadata(classSymbol);
const metadata = resolvedMetadata && resolvedMetadata.metadata; if (!data) {
if (!metadata) { return {
return {}; kind: DiagnosticKind.Error,
message: `No metadata found for '${classSymbol.name}' in ${fileName}.`,
span: template.span,
};
} }
const rawHtmlParser = new HtmlParser(); const htmlParser = new I18NHtmlParser(new HtmlParser());
const htmlParser = new I18NHtmlParser(rawHtmlParser);
const expressionParser = new Parser(new Lexer()); const expressionParser = new Parser(new Lexer());
const config = new CompilerConfig();
const parser = new TemplateParser( const parser = new TemplateParser(
config, this.reflector, expressionParser, new DomElementSchemaRegistry(), htmlParser, new CompilerConfig(), this.reflector, expressionParser, new DomElementSchemaRegistry(),
null !, []); htmlParser,
const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true}); null !, // console
const errors: Diagnostic[]|undefined = undefined; [] // tranforms
const ngModule = this.analyzedModules.ngModuleByPipeOrDirective.get(template.type) || );
// Reported by the the declaration diagnostics. const htmlResult = htmlParser.parse(template.source, fileName, {
findSuitableDefaultModule(this.analyzedModules); tokenizeExpansionForms: true,
if (!ngModule) { });
return {}; 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 { return {
htmlAst: htmlResult.rootNodes, htmlAst: htmlResult.rootNodes,
templateAst: parseResult.templateAst, templateAst: parseResult.templateAst,
directive: metadata, directives, pipes, directive: data.metadata, directives, pipes,
parseErrors: parseResult.errors, expressionParser, errors parseErrors: parseResult.errors, expressionParser, template,
}; };
} catch (e) { } catch (e) {
const span = e.fileName === template.fileName && template.query.getSpanAt(e.line, e.column) ||
template.span;
return { return {
errors: [{ kind: DiagnosticKind.Error,
kind: DiagnosticKind.Error, message: e.message,
message: e.message, span, span:
}], e.fileName === fileName && template.query.getSpanAt(e.line, e.column) || template.span,
}; };
} }
} }

View File

@ -10,7 +10,7 @@ import {AstPath, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, Dire
import {DiagnosticTemplateInfo} from '@angular/compiler-cli/src/language_services'; import {DiagnosticTemplateInfo} from '@angular/compiler-cli/src/language_services';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {SelectorInfo, TemplateInfo} from './common'; import {AstResult, SelectorInfo} from './common';
import {Span} from './types'; import {Span} from './types';
export interface SpanHolder { export interface SpanHolder {
@ -67,7 +67,7 @@ export function hasTemplateReference(type: CompileTypeMetadata): boolean {
return false; return false;
} }
export function getSelectors(info: TemplateInfo): SelectorInfo { export function getSelectors(info: AstResult): SelectorInfo {
const map = new Map<CssSelector, CompileDirectiveSummary>(); const map = new Map<CssSelector, CompileDirectiveSummary>();
const selectors: CssSelector[] = flatten(info.directives.map(directive => { const selectors: CssSelector[] = flatten(info.directives.map(directive => {
const selectors: CssSelector[] = CssSelector.parse(directive.selector !); const selectors: CssSelector[] = CssSelector.parse(directive.selector !);
@ -113,9 +113,9 @@ export function isTypescriptVersion(low: string, high?: string) {
return true; return true;
} }
export function diagnosticInfoFromTemplateInfo(info: TemplateInfo): DiagnosticTemplateInfo { export function diagnosticInfoFromTemplateInfo(info: AstResult): DiagnosticTemplateInfo {
return { return {
fileName: info.fileName, fileName: info.template.fileName,
offset: info.template.span.start, offset: info.template.span.start,
query: info.template.query, query: info.template.query,
members: info.template.members, members: info.template.members,