fix(language-service): remove asserts for non-null expressions (#16422)

Reworked some of the code so asserts are no longer necessary.
Added additional and potentially redundant checks
Added checks where the null checker found real problems.

PR Close #16422
This commit is contained in:
Chuck Jazdzewski 2017-04-28 15:10:30 -07:00 committed by Miško Hevery
parent 270d694ed2
commit 253345c0c0
12 changed files with 241 additions and 195 deletions

View File

@ -13,7 +13,9 @@ export class AstPath<T> {
get head(): T|undefined { return this.path[0]; } get head(): T|undefined { return this.path[0]; }
get tail(): T|undefined { return this.path[this.path.length - 1]; } get tail(): T|undefined { return this.path[this.path.length - 1]; }
parentOf(node: T): T|undefined { return this.path[this.path.indexOf(node) - 1]; } parentOf(node: T|undefined): T|undefined {
return node && this.path[this.path.indexOf(node) - 1];
}
childOf(node: T): T|undefined { return this.path[this.path.indexOf(node) + 1]; } childOf(node: T): T|undefined { return this.path[this.path.indexOf(node) + 1]; }
first<N extends T>(ctor: {new (...args: any[]): N}): N|undefined { first<N extends T>(ctor: {new (...args: any[]): N}): N|undefined {

View File

@ -33,23 +33,24 @@ export function getTemplateCompletions(templateInfo: TemplateInfo): Completions|
let result: Completions|undefined = undefined; let result: Completions|undefined = undefined;
let {htmlAst, templateAst, template} = templateInfo; let {htmlAst, templateAst, 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.
let templatePosition = templateInfo.position ! - template.span.start; if (templateInfo.position != null) {
let templatePosition = templateInfo.position - template.span.start;
let path = new HtmlAstPath(htmlAst, templatePosition); let path = new HtmlAstPath(htmlAst, templatePosition);
let mostSpecific = path.tail; let mostSpecific = path.tail;
if (path.empty) { 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 bracked */) { 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);
@ -65,7 +66,7 @@ export function getTemplateCompletions(templateInfo: TemplateInfo): Completions|
}, },
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;
@ -93,11 +94,12 @@ export function getTemplateCompletions(templateInfo: TemplateInfo): Completions|
}, },
null); null);
} }
}
return result; return result;
} }
function attributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions|undefined { function attributeCompletions(info: TemplateInfo, path: HtmlAstPath): 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);
} }
@ -213,11 +215,12 @@ function elementCompletions(info: TemplateInfo, path: HtmlAstPath): Completions|
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
let directiveElements = let directiveElements = getSelectors(info)
getSelectors(info).selectors.map(selector => selector.element).filter(name => !!name); .selectors.map(selector => selector.element)
.filter(name => !!name) as string[];
let components = let components =
directiveElements.map<Completion>(name => ({kind: 'component', name: name !, sort: name !})); directiveElements.map<Completion>(name => ({kind: 'component', name, sort: name}));
let htmlElements = htmlNames.map<Completion>(name => ({kind: 'element', name: name, sort: name})); let htmlElements = htmlNames.map<Completion>(name => ({kind: 'element', name: name, sort: name}));
// Return components and html elements // Return components and html elements
@ -262,25 +265,25 @@ function voidElementAttributeCompletions(info: TemplateInfo, path: HtmlAstPath):
undefined { undefined {
let tail = path.tail; let tail = path.tail;
if (tail instanceof Text) { if (tail instanceof Text) {
let match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/) !; let match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/);
// The position must be after the match, otherwise we are still in a place where elements // The position must be after the match, otherwise we are still in a place where elements
// are expected (such as `<|a` or `<a|`; we only want attributes for `<a |` or after). // are expected (such as `<|a` or `<a|`; we only want attributes for `<a |` or after).
if (match && path.position >= match.index ! + match[0].length + tail.sourceSpan.start.offset) { if (match &&
path.position >= (match.index || 0) + match[0].length + tail.sourceSpan.start.offset) {
return attributeCompletionsForElement(info, match[3]); return attributeCompletionsForElement(info, match[3]);
} }
} }
} }
class ExpressionVisitor extends NullTemplateVisitor { class ExpressionVisitor extends NullTemplateVisitor {
private getExpressionScope: () => SymbolTable;
result: Completions; result: Completions;
constructor( constructor(
private info: TemplateInfo, private position: number, private attr?: Attribute, private info: TemplateInfo, private position: number, private attr?: Attribute,
private getExpressionScope?: () => SymbolTable) { getExpressionScope?: () => SymbolTable) {
super(); super();
if (!getExpressionScope) { this.getExpressionScope = getExpressionScope || (() => info.template.members);
this.getExpressionScope = () => info.template.members;
}
} }
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void { visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
@ -311,7 +314,8 @@ class ExpressionVisitor extends NullTemplateVisitor {
this.info.expressionParser.parseTemplateBindings(key, this.attr.value, null); this.info.expressionParser.parseTemplateBindings(key, this.attr.value, null);
// find the template binding that contains the position // find the template binding that contains the position
const valueRelativePosition = this.position - this.attr.valueSpan !.start.offset - 1; if (!this.attr.valueSpan) return;
const valueRelativePosition = this.position - this.attr.valueSpan.start.offset - 1;
const bindings = templateBindingResult.templateBindings; const bindings = templateBindingResult.templateBindings;
const binding = const binding =
bindings.find( bindings.find(
@ -340,11 +344,13 @@ class ExpressionVisitor extends NullTemplateVisitor {
// We are after the '=' in a let clause. The valid values here are the members of the // We are after the '=' in a let clause. The valid values here are the members of the
// template reference's type parameter. // template reference's type parameter.
const directiveMetadata = selectorInfo.map.get(selector); const directiveMetadata = selectorInfo.map.get(selector);
if (directiveMetadata) {
const contextTable = const contextTable =
this.info.template.query.getTemplateContext(directiveMetadata !.type.reference); this.info.template.query.getTemplateContext(directiveMetadata.type.reference);
if (contextTable) { if (contextTable) {
this.result = this.symbolsToCompletions(contextTable.values()); this.result = this.symbolsToCompletions(contextTable.values());
} }
}
} else if (binding.key && valueRelativePosition <= (binding.key.length - key.length)) { } else if (binding.key && valueRelativePosition <= (binding.key.length - key.length)) {
keyCompletions(); keyCompletions();
} }
@ -371,7 +377,7 @@ class ExpressionVisitor extends NullTemplateVisitor {
const expressionPosition = this.position - ast.sourceSpan.start.offset; const expressionPosition = this.position - ast.sourceSpan.start.offset;
if (inSpan(expressionPosition, ast.value.span)) { if (inSpan(expressionPosition, ast.value.span)) {
const completions = getExpressionCompletions( const completions = getExpressionCompletions(
this.getExpressionScope !(), ast.value, expressionPosition, this.info.template.query); this.getExpressionScope(), ast.value, expressionPosition, this.info.template.query);
if (completions) { if (completions) {
this.result = this.symbolsToCompletions(completions); this.result = this.symbolsToCompletions(completions);
} }
@ -380,8 +386,8 @@ class ExpressionVisitor extends NullTemplateVisitor {
private attributeValueCompletions(value: AST, position?: number) { private attributeValueCompletions(value: AST, position?: number) {
const symbols = getExpressionCompletions( const symbols = getExpressionCompletions(
this.getExpressionScope !(), value, this.getExpressionScope(), value, position == null ? this.attributeValuePosition : position,
position == null ? this.attributeValuePosition : position, this.info.template.query); this.info.template.query);
if (symbols) { if (symbols) {
this.result = this.symbolsToCompletions(symbols); this.result = this.symbolsToCompletions(symbols);
} }
@ -393,12 +399,13 @@ class ExpressionVisitor extends NullTemplateVisitor {
} }
private get attributeValuePosition() { private get attributeValuePosition() {
return this.position - this.attr !.valueSpan !.start.offset - 1; if (this.attr && this.attr.valueSpan) {
return this.position - this.attr.valueSpan.start.offset - 1;
}
return 0;
} }
} }
function getSourceText(template: TemplateSource, span: Span): string { function getSourceText(template: TemplateSource, span: Span): string {
return template.source.substring(span.start, span.end); return template.source.substring(span.start, span.end);
} }

View File

@ -29,7 +29,7 @@ export function getTemplateDiagnostics(
results.push(...ast.parseErrors.map<Diagnostic>( results.push(...ast.parseErrors.map<Diagnostic>(
e => ({ e => ({
kind: DiagnosticKind.Error, kind: 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) { } else if (ast.templateAst) {
@ -91,20 +91,24 @@ export function getDeclarationDiagnostics(
function getTemplateExpressionDiagnostics( function getTemplateExpressionDiagnostics(
template: TemplateSource, astResult: AstResult): Diagnostics { template: TemplateSource, astResult: AstResult): Diagnostics {
if (astResult.htmlAst && astResult.directive && astResult.directives && astResult.pipes &&
astResult.templateAst && astResult.expressionParser) {
const info: TemplateInfo = { const info: TemplateInfo = {
template, template,
htmlAst: astResult.htmlAst !, htmlAst: astResult.htmlAst,
directive: astResult.directive !, directive: astResult.directive,
directives: astResult.directives !, directives: astResult.directives,
pipes: astResult.pipes !, pipes: astResult.pipes,
templateAst: astResult.templateAst !, templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser ! expressionParser: astResult.expressionParser
}; };
const visitor = new ExpressionDiagnosticsVisitor( const visitor = new ExpressionDiagnosticsVisitor(
info, (path: TemplateAstPath, includeEvent: boolean) => info, (path: TemplateAstPath, includeEvent: boolean) =>
getExpressionScope(info, path, includeEvent)); getExpressionScope(info, path, includeEvent));
templateVisitAll(visitor, astResult.templateAst !); templateVisitAll(visitor, astResult.templateAst !);
return visitor.diagnostics; return visitor.diagnostics;
}
return [];
} }
class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor { class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
@ -158,11 +162,11 @@ class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
if (context && !context.has(ast.value)) { if (context && !context.has(ast.value)) {
if (ast.value === '$implicit') { if (ast.value === '$implicit') {
this.reportError( this.reportError(
'The template context does not have an implicit value', spanOf(ast.sourceSpan) !); 'The template context does not have an implicit value', spanOf(ast.sourceSpan));
} else { } else {
this.reportError( this.reportError(
`The template context does not defined a member called '${ast.value}'`, `The template context does not defined a member called '${ast.value}'`,
spanOf(ast.sourceSpan) !); spanOf(ast.sourceSpan));
} }
} }
} }
@ -233,12 +237,14 @@ class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
} }
} }
private reportError(message: string, span: Span) { private reportError(message: string, span: Span|undefined) {
if (span) {
this.diagnostics.push({ this.diagnostics.push({
span: offsetSpan(span, this.info.template.span.start), span: offsetSpan(span, this.info.template.span.start),
kind: DiagnosticKind.Error, message kind: DiagnosticKind.Error, message
}); });
} }
}
private reportWarning(message: string, span: Span) { private reportWarning(message: string, span: Span) {
this.diagnostics.push({ this.diagnostics.push({

View File

@ -495,8 +495,9 @@ class AstType implements ExpressionVisitor {
// The type of a method is the selected methods result type. // The type of a method is the selected methods result type.
const method = receiverType.members().get(ast.name); const method = receiverType.members().get(ast.name);
if (!method) return this.reportError(`Unknown method ${ast.name}`, ast); if (!method) return this.reportError(`Unknown method ${ast.name}`, ast);
if (!method.type !.callable) return this.reportError(`Member ${ast.name} is not callable`, ast); if (!method.type) return this.reportError(`Could not find a type for ${ast.name}`, ast);
const signature = method.type !.selectSignature(ast.args.map(arg => this.getType(arg))); if (!method.type.callable) return this.reportError(`Member ${ast.name} is not callable`, ast);
const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg)));
if (!signature) if (!signature)
return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast); return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast);
return signature.result; return signature.result;
@ -601,7 +602,9 @@ function visitChildren(ast: AST, visitor: ExpressionVisitor) {
visit(ast.falseExp); visit(ast.falseExp);
}, },
visitFunctionCall(ast) { visitFunctionCall(ast) {
visit(ast.target !); if (ast.target) {
visit(ast.target);
}
visitAll(ast.args); visitAll(ast.args);
}, },
visitImplicitReceiver(ast) {}, visitImplicitReceiver(ast) {},
@ -676,7 +679,7 @@ function getReferences(info: TemplateInfo): SymbolDeclaration[] {
function processReferences(references: ReferenceAst[]) { function processReferences(references: ReferenceAst[]) {
for (const reference of references) { for (const reference of references) {
let type: Symbol = undefined !; let type: Symbol|undefined = undefined;
if (reference.value) { if (reference.value) {
type = info.template.query.getTypeSymbol(tokenReference(reference.value)); type = info.template.query.getTypeSymbol(tokenReference(reference.value));
} }
@ -721,7 +724,7 @@ function getVarDeclarations(info: TemplateInfo, path: TemplateAstPath): SymbolDe
.find(c => !!c); .find(c => !!c);
// Determine the type of the context field referenced by variable.value. // Determine the type of the context field referenced by variable.value.
let type: Symbol = undefined !; let type: Symbol|undefined = undefined;
if (context) { if (context) {
const value = context.get(variable.value); const value = context.get(variable.value);
if (value) { if (value) {
@ -762,7 +765,10 @@ function refinedVariableType(
const bindingType = const bindingType =
new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value); new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value);
if (bindingType) { if (bindingType) {
return info.template.query.getElementType(bindingType) !; const result = info.template.query.getElementType(bindingType);
if (result) {
return result;
}
} }
} }
} }

View File

@ -81,24 +81,25 @@ class LanguageServiceImpl implements LanguageService {
let template = this.host.getTemplateAt(fileName, position); let template = this.host.getTemplateAt(fileName, position);
if (template) { if (template) {
let astResult = this.getTemplateAst(template, fileName); let astResult = this.getTemplateAst(template, fileName);
if (astResult && astResult.htmlAst && astResult.templateAst) if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive &&
astResult.directives && astResult.pipes && astResult.expressionParser)
return { return {
position, position,
fileName, fileName,
template, template,
htmlAst: astResult.htmlAst, htmlAst: astResult.htmlAst,
directive: astResult.directive !, directive: astResult.directive,
directives: astResult.directives !, directives: astResult.directives,
pipes: astResult.pipes !, pipes: astResult.pipes,
templateAst: astResult.templateAst, templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser ! expressionParser: astResult.expressionParser
}; };
} }
return undefined; return undefined;
} }
getTemplateAst(template: TemplateSource, contextFile: string): AstResult { getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
let result: AstResult = undefined !; let result: AstResult|undefined = undefined;
try { try {
const resolvedMetadata = const resolvedMetadata =
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any); this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
@ -112,7 +113,7 @@ class LanguageServiceImpl implements LanguageService {
config, expressionParser, new DomElementSchemaRegistry(), htmlParser, null !, []); config, expressionParser, new DomElementSchemaRegistry(), htmlParser, null !, []);
const htmlResult = htmlParser.parse(template.source, '', true); const htmlResult = htmlParser.parse(template.source, '', true);
const analyzedModules = this.host.getAnalyzedModules(); const analyzedModules = this.host.getAnalyzedModules();
let errors: Diagnostic[] = undefined !; let errors: Diagnostic[]|undefined = undefined;
let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type); let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type);
if (!ngModule) { if (!ngModule) {
// Reported by the the declaration diagnostics. // Reported by the the declaration diagnostics.
@ -121,8 +122,7 @@ class LanguageServiceImpl implements LanguageService {
if (ngModule) { if (ngModule) {
const resolvedDirectives = ngModule.transitiveModule.directives.map( const resolvedDirectives = ngModule.transitiveModule.directives.map(
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference)); d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference));
const directives = const directives = removeMissing(resolvedDirectives).map(d => d.metadata.toSummary());
resolvedDirectives.filter(d => d !== null).map(d => d !.metadata.toSummary());
const pipes = ngModule.transitiveModule.pipes.map( const pipes = ngModule.transitiveModule.pipes.map(
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary()); p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
const schemas = ngModule.schemas; const schemas = ngModule.schemas;
@ -142,10 +142,14 @@ class LanguageServiceImpl implements LanguageService {
} }
result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]}; result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]};
} }
return result; return result || {};
} }
} }
function removeMissing<T>(values: (T | null | undefined)[]): T[] {
return values.filter(e => !!e) as T[];
}
function uniqueBySpan < T extends { function uniqueBySpan < T extends {
span: Span; span: Span;
} }
@ -169,8 +173,8 @@ function uniqueBySpan < T extends {
} }
} }
function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata { function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata|undefined {
let result: CompileNgModuleMetadata = undefined !; let result: CompileNgModuleMetadata|undefined = undefined;
let resultSize = 0; let resultSize = 0;
for (const module of modules.ngModules) { for (const module of modules.ngModules) {
const moduleSize = module.transitiveModule.directives.length; const moduleSize = module.transitiveModule.directives.length;

View File

@ -21,23 +21,26 @@ export interface SymbolInfo {
} }
export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
const templatePosition = info.position ! - info.template.span.start; if (!info.position) return undefined;
const templatePosition = info.position - info.template.span.start;
const path = new TemplateAstPath(info.templateAst, templatePosition); const path = new TemplateAstPath(info.templateAst, templatePosition);
if (path.tail) { if (path.tail) {
let symbol: Symbol = undefined !; let symbol: Symbol|undefined = undefined;
let span: Span = 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);
if (attribute) { if (attribute) {
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) { if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
const scope = getExpressionScope(info, path, inEvent); const scope = getExpressionScope(info, path, inEvent);
const expressionOffset = attribute.valueSpan !.start.offset + 1; if (attribute.valueSpan) {
const expressionOffset = attribute.valueSpan.start.offset + 1;
const result = getExpressionSymbol( const result = getExpressionSymbol(
scope, ast, templatePosition - expressionOffset, info.template.query); scope, ast, templatePosition - expressionOffset, info.template.query);
if (result) { if (result) {
symbol = result.symbol; symbol = result.symbol;
span = offsetSpan(result.span, expressionOffset); span = offsetSpan(result.span, expressionOffset);
} }
}
return true; return true;
} }
} }
@ -52,28 +55,28 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
if (component) { if (component) {
symbol = info.template.query.getTypeSymbol(component.directive.type.reference); symbol = info.template.query.getTypeSymbol(component.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'component'); symbol = symbol && new OverrideKindSymbol(symbol, 'component');
span = spanOf(ast) !; span = spanOf(ast);
} else { } else {
// Find a directive that matches the element name // Find a directive that matches the element name
const directive = const directive = ast.directives.find(
ast.directives.find(d => d.directive.selector !.indexOf(ast.name) >= 0); d => d.directive.selector != null && d.directive.selector.indexOf(ast.name) >= 0);
if (directive) { if (directive) {
symbol = info.template.query.getTypeSymbol(directive.directive.type.reference); symbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'directive'); symbol = symbol && new OverrideKindSymbol(symbol, 'directive');
span = spanOf(ast) !; span = spanOf(ast);
} }
} }
}, },
visitReference(ast) { visitReference(ast) {
symbol = info.template.query.getTypeSymbol(tokenReference(ast.value)); symbol = info.template.query.getTypeSymbol(tokenReference(ast.value));
span = spanOf(ast) !; span = spanOf(ast);
}, },
visitVariable(ast) {}, visitVariable(ast) {},
visitEvent(ast) { visitEvent(ast) {
if (!attributeValueSymbol(ast.handler, /* inEvent */ true)) { if (!attributeValueSymbol(ast.handler, /* inEvent */ true)) {
symbol = findOutputBinding(info, path, ast) !; symbol = findOutputBinding(info, path, ast);
symbol = symbol && new OverrideKindSymbol(symbol, 'event'); symbol = symbol && new OverrideKindSymbol(symbol, 'event');
span = spanOf(ast) !; span = spanOf(ast);
} }
}, },
visitElementProperty(ast) { attributeValueSymbol(ast.value); }, visitElementProperty(ast) { attributeValueSymbol(ast.value); },
@ -93,12 +96,12 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
visitText(ast) {}, visitText(ast) {},
visitDirective(ast) { visitDirective(ast) {
symbol = info.template.query.getTypeSymbol(ast.directive.type.reference); symbol = info.template.query.getTypeSymbol(ast.directive.type.reference);
span = spanOf(ast) !; span = spanOf(ast);
}, },
visitDirectiveProperty(ast) { visitDirectiveProperty(ast) {
if (!attributeValueSymbol(ast.value)) { if (!attributeValueSymbol(ast.value)) {
symbol = findInputBinding(info, path, ast) !; symbol = findInputBinding(info, path, ast);
span = spanOf(ast) !; span = spanOf(ast);
} }
} }
}, },
@ -110,9 +113,11 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
} }
function findAttribute(info: TemplateInfo): Attribute|undefined { function findAttribute(info: TemplateInfo): Attribute|undefined {
const templatePosition = info.position ! - info.template.span.start; if (info.position) {
const templatePosition = info.position - info.template.span.start;
const path = new HtmlAstPath(info.htmlAst, templatePosition); const path = new HtmlAstPath(info.htmlAst, templatePosition);
return path.first(Attribute); return path.first(Attribute);
}
} }
function findInputBinding( function findInputBinding(

View File

@ -22,18 +22,24 @@ class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost {
if (snapshot) { if (snapshot) {
return snapshot.getText(0, snapshot.getLength()); return snapshot.getText(0, snapshot.getLength());
} }
// Typescript readFile() declaration should be `readFile(fileName: string): string | undefined
return undefined !; return undefined !;
} }
directoryExists: (directoryName: string) => boolean; directoryExists: (directoryName: string) => boolean;
} }
// This reflector host's purpose is to first set verboseInvalidExpressions to true so the
// reflector will collect errors instead of throwing, and second to all deferring the creation
// of the program until it is actually needed.
export class ReflectorHost extends CompilerHost { export class ReflectorHost extends CompilerHost {
constructor( constructor(
private getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost, private getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost,
options: AngularCompilerOptions) { options: AngularCompilerOptions) {
super( super(
null !, options, // The ancestor value for program is overridden below so passing null here is safe.
/* program */ null !, options,
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)), new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)),
{verboseInvalidExpression: true}); {verboseInvalidExpression: true});
} }

View File

@ -104,10 +104,10 @@ class TemplateAstPathBuilder extends TemplateAstChildVisitor {
constructor(private position: number, private allowWidening: boolean) { super(); } constructor(private position: number, private allowWidening: boolean) { super(); }
visit(ast: TemplateAst, context: any): any { visit(ast: TemplateAst, context: any): any {
let span = spanOf(ast) !; let span = spanOf(ast);
if (inSpan(this.position, span)) { if (inSpan(this.position, span)) {
const len = this.path.length; const len = this.path.length;
if (!len || this.allowWidening || isNarrower(span, spanOf(this.path[len - 1]) !)) { if (!len || this.allowWidening || isNarrower(span, spanOf(this.path[len - 1]))) {
this.path.push(ast); this.path.push(ast);
} }
} else { } else {

View File

@ -170,14 +170,16 @@ export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageS
} }
function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic { function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic {
return { const result = {
file, file,
start: d.span.start, start: d.span.start,
length: d.span.end - d.span.start, length: d.span.end - d.span.start,
messageText: d.message, messageText: d.message,
category: ts.DiagnosticCategory.Error, category: ts.DiagnosticCategory.Error,
code: 0 code: 0,
source: 'ng'
}; };
return result;
} }
function tryOperation<T>(attempting: string, callback: () => T): T|null { function tryOperation<T>(attempting: string, callback: () => T): T|null {
@ -229,7 +231,7 @@ export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageS
if (ours) { if (ours) {
const displayParts: ts.SymbolDisplayPart[] = []; const displayParts: ts.SymbolDisplayPart[] = [];
for (const part of ours.text) { for (const part of ours.text) {
displayParts.push({kind: part.language !, text: part.text}); displayParts.push({kind: part.language || 'angular', text: part.text});
} }
const tags = base && (<any>base).tags; const tags = base && (<any>base).tags;
base = <any>{ base = <any>{

View File

@ -8,8 +8,6 @@
import {CompileDirectiveMetadata, CompileMetadataResolver, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; import {CompileDirectiveMetadata, CompileMetadataResolver, NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
/** /**
* The range of a span of text in a source file. * The range of a span of text in a source file.
* *
@ -455,7 +453,7 @@ export interface LanguageServiceHost {
* refers to a template file then the `position` should be ignored. If the `position` is not in a * refers to a template file then the `position` should be ignored. If the `position` is not in a
* template literal string then this method should return `undefined`. * template literal string then this method should return `undefined`.
*/ */
getTemplateAt(fileName: string, position: number): TemplateSource /* |undefined */; getTemplateAt(fileName: string, position: number): TemplateSource|undefined;
/** /**
* Return the template source information for all templates in `fileName` or for `fileName` if it * Return the template source information for all templates in `fileName` or for `fileName` if it

View File

@ -74,22 +74,22 @@ export class DummyResourceLoader extends ResourceLoader {
* @experimental * @experimental
*/ */
export class TypeScriptServiceHost implements LanguageServiceHost { export class TypeScriptServiceHost implements LanguageServiceHost {
private _resolver: CompileMetadataResolver; private _resolver: CompileMetadataResolver|null;
private _staticSymbolCache = new StaticSymbolCache(); private _staticSymbolCache = new StaticSymbolCache();
private _summaryResolver: AotSummaryResolver; private _summaryResolver: AotSummaryResolver;
private _staticSymbolResolver: StaticSymbolResolver; private _staticSymbolResolver: StaticSymbolResolver;
private _reflector: StaticReflector; private _reflector: StaticReflector|null;
private _reflectorHost: ReflectorHost; private _reflectorHost: ReflectorHost;
private _checker: ts.TypeChecker; private _checker: ts.TypeChecker|null;
private _typeCache: Symbol[] = []; private _typeCache: Symbol[] = [];
private context: string|undefined; private context: string|undefined;
private lastProgram: ts.Program|undefined; private lastProgram: ts.Program|undefined;
private modulesOutOfDate: boolean = true; private modulesOutOfDate: boolean = true;
private analyzedModules: NgAnalyzedModules; private analyzedModules: NgAnalyzedModules|null;
private service: LanguageService; private service: LanguageService;
private fileToComponent: Map<string, StaticSymbol>; private fileToComponent: Map<string, StaticSymbol>|null;
private templateReferences: string[]; private templateReferences: string[]|null;
private collectedErrors: Map<string, any[]>; private collectedErrors: Map<string, any[]>|null;
private fileVersions = new Map<string, string>(); private fileVersions = new Map<string, string>();
constructor(private host: ts.LanguageServiceHost, private tsService: ts.LanguageService) {} constructor(private host: ts.LanguageServiceHost, private tsService: ts.LanguageService) {}
@ -127,28 +127,28 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
getTemplateReferences(): string[] { getTemplateReferences(): string[] {
this.ensureTemplateMap(); this.ensureTemplateMap();
return this.templateReferences; return this.templateReferences || [];
} }
getTemplateAt(fileName: string, position: number): TemplateSource { getTemplateAt(fileName: string, position: number): TemplateSource|undefined {
let sourceFile = this.getSourceFile(fileName); let sourceFile = this.getSourceFile(fileName);
if (sourceFile) { if (sourceFile) {
this.context = sourceFile.fileName; this.context = sourceFile.fileName;
let node = this.findNode(sourceFile, position); let node = this.findNode(sourceFile, position);
if (node) { if (node) {
return this.getSourceFromNode( return this.getSourceFromNode(
fileName, this.host.getScriptVersion(sourceFile.fileName), node) !; fileName, this.host.getScriptVersion(sourceFile.fileName), node);
} }
} else { } else {
this.ensureTemplateMap(); this.ensureTemplateMap();
// TODO: Cannocalize the file? // TODO: Cannocalize the file?
const componentType = this.fileToComponent.get(fileName); const componentType = this.fileToComponent !.get(fileName);
if (componentType) { if (componentType) {
return this.getSourceFromType( return this.getSourceFromType(
fileName, this.host.getScriptVersion(fileName), componentType) !; fileName, this.host.getScriptVersion(fileName), componentType);
} }
} }
return null !; return undefined;
} }
getAnalyzedModules(): NgAnalyzedModules { getAnalyzedModules(): NgAnalyzedModules {
@ -172,7 +172,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
getTemplates(fileName: string): TemplateSources { getTemplates(fileName: string): TemplateSources {
this.ensureTemplateMap(); this.ensureTemplateMap();
const componentType = this.fileToComponent.get(fileName); const componentType = this.fileToComponent !.get(fileName);
if (componentType) { if (componentType) {
const templateSource = this.getTemplateAt(fileName, 0); const templateSource = this.getTemplateAt(fileName, 0);
if (templateSource) { if (templateSource) {
@ -225,10 +225,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
updateAnalyzedModules() { updateAnalyzedModules() {
this.validate(); this.validate();
if (this.modulesOutOfDate) { if (this.modulesOutOfDate) {
this.analyzedModules = null !; this.analyzedModules = null;
this._reflector = null !; this._reflector = null;
this.templateReferences = null !; this.templateReferences = null;
this.fileToComponent = null !; this.fileToComponent = null;
this.ensureAnalyzedModules(); this.ensureAnalyzedModules();
this.modulesOutOfDate = false; this.modulesOutOfDate = false;
} }
@ -273,10 +273,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
private clearCaches() { private clearCaches() {
this._checker = null !; this._checker = null;
this._typeCache = []; this._typeCache = [];
this._resolver = null !; this._resolver = null;
this.collectedErrors = null !; this.collectedErrors = null;
this.modulesOutOfDate = true; this.modulesOutOfDate = true;
} }
@ -345,7 +345,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (declaration && declaration.name) { if (declaration && declaration.name) {
const sourceFile = this.getSourceFile(fileName); const sourceFile = this.getSourceFile(fileName);
return this.getSourceFromDeclaration( return this.getSourceFromDeclaration(
fileName, version, this.stringOf(node) !, shrink(spanOf(node)), fileName, version, this.stringOf(node) || '', shrink(spanOf(node)),
this.reflector.getStaticSymbol(sourceFile.fileName, declaration.name.text), this.reflector.getStaticSymbol(sourceFile.fileName, declaration.name.text),
declaration, node, sourceFile); declaration, node, sourceFile);
} }
@ -359,11 +359,13 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
let result: TemplateSource|undefined = undefined; let result: TemplateSource|undefined = undefined;
const declaration = this.getTemplateClassFromStaticSymbol(type); const declaration = this.getTemplateClassFromStaticSymbol(type);
if (declaration) { if (declaration) {
const snapshot = this.host.getScriptSnapshot(fileName) !; const snapshot = this.host.getScriptSnapshot(fileName);
if (snapshot) {
const source = snapshot.getText(0, snapshot.getLength()); const source = snapshot.getText(0, snapshot.getLength());
result = this.getSourceFromDeclaration( result = this.getSourceFromDeclaration(
fileName, version, source, {start: 0, end: source.length}, type, declaration, declaration, fileName, version, source, {start: 0, end: source.length}, type, declaration,
declaration.getSourceFile()); declaration, declaration.getSourceFile());
}
} }
return result; return result;
} }
@ -398,9 +400,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return result; return result;
} }
private collectError(error: any, filePath: string) { private collectError(error: any, filePath: string|null) {
if (filePath) {
let errorMap = this.collectedErrors; let errorMap = this.collectedErrors;
if (!errorMap) { if (!errorMap || !this.collectedErrors) {
errorMap = this.collectedErrors = new Map(); errorMap = this.collectedErrors = new Map();
} }
let errors = errorMap.get(filePath); let errors = errorMap.get(filePath);
@ -410,15 +413,16 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
errors.push(error); errors.push(error);
} }
}
private get staticSymbolResolver(): StaticSymbolResolver { private get staticSymbolResolver(): StaticSymbolResolver {
let result = this._staticSymbolResolver; let result = this._staticSymbolResolver;
if (!result) { if (!result) {
this._summaryResolver = new AotSummaryResolver( this._summaryResolver = new AotSummaryResolver(
{ {
loadSummary(filePath: string) { return null !; }, loadSummary(filePath: string) { return null; },
isSourceFile(sourceFilePath: string) { return true !; }, isSourceFile(sourceFilePath: string) { return true; },
getOutputFileName(sourceFilePath: string) { return null !; } getOutputFileName(sourceFilePath: string) { return sourceFilePath; }
}, },
this._staticSymbolCache); this._staticSymbolCache);
result = this._staticSymbolResolver = new StaticSymbolResolver( result = this._staticSymbolResolver = new StaticSymbolResolver(
@ -445,7 +449,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
const declarationNode = ts.forEachChild(source, child => { const declarationNode = ts.forEachChild(source, child => {
if (child.kind === ts.SyntaxKind.ClassDeclaration) { if (child.kind === ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = child as ts.ClassDeclaration; const classDeclaration = child as ts.ClassDeclaration;
if (classDeclaration.name !.text === type.name) { if (classDeclaration.name != null && classDeclaration.name.text === type.name) {
return classDeclaration; return classDeclaration;
} }
} }
@ -614,7 +618,7 @@ class TypeScriptSymbolQuery implements SymbolQuery {
private program: ts.Program, private checker: ts.TypeChecker, private source: ts.SourceFile, private program: ts.Program, private checker: ts.TypeChecker, private source: ts.SourceFile,
private fetchPipes: () => SymbolTable) {} private fetchPipes: () => SymbolTable) {}
getTypeKind(symbol: Symbol): BuiltinType { return typeKindOf(this.getTsTypeOf(symbol) !); } getTypeKind(symbol: Symbol): BuiltinType { return typeKindOf(this.getTsTypeOf(symbol)); }
getBuiltinType(kind: BuiltinType): Symbol { getBuiltinType(kind: BuiltinType): Symbol {
// TODO: Replace with typeChecker API when available. // TODO: Replace with typeChecker API when available.
@ -1303,7 +1307,7 @@ function getTypeParameterOf(type: ts.Type, name: string): ts.Type|undefined {
} }
} }
function typeKindOf(type: ts.Type): BuiltinType { function typeKindOf(type: ts.Type | undefined): BuiltinType {
if (type) { if (type) {
if (type.flags & ts.TypeFlags.Any) { if (type.flags & ts.TypeFlags.Any) {
return BuiltinType.Any; return BuiltinType.Any;
@ -1318,17 +1322,19 @@ function typeKindOf(type: ts.Type): BuiltinType {
return BuiltinType.Null; return BuiltinType.Null;
} else if (type.flags & ts.TypeFlags.Union) { } else if (type.flags & ts.TypeFlags.Union) {
// If all the constituent types of a union are the same kind, it is also that kind. // If all the constituent types of a union are the same kind, it is also that kind.
let candidate: BuiltinType = undefined !; let candidate: BuiltinType|null = null;
const unionType = type as ts.UnionType; const unionType = type as ts.UnionType;
if (unionType.types.length > 0) { if (unionType.types.length > 0) {
candidate = typeKindOf(unionType.types[0]) !; candidate = typeKindOf(unionType.types[0]);
for (const subType of unionType.types) { for (const subType of unionType.types) {
if (candidate != typeKindOf(subType)) { if (candidate != typeKindOf(subType)) {
return BuiltinType.Other; return BuiltinType.Other;
} }
} }
} }
if (candidate != null) {
return candidate; return candidate;
}
} else if (type.flags & ts.TypeFlags.TypeParameter) { } else if (type.flags & ts.TypeFlags.TypeParameter) {
return BuiltinType.Unbound; return BuiltinType.Unbound;
} }

View File

@ -21,6 +21,9 @@ export function isParseSourceSpan(value: any): value is ParseSourceSpan {
return value && !!value.start; return value && !!value.start;
} }
export function spanOf(span: SpanHolder): Span;
export function spanOf(span: ParseSourceSpan): Span;
export function spanOf(span: SpanHolder | ParseSourceSpan | undefined): Span|undefined;
export function spanOf(span?: SpanHolder | ParseSourceSpan): Span|undefined { export function spanOf(span?: SpanHolder | ParseSourceSpan): Span|undefined {
if (!span) return undefined; if (!span) return undefined;
if (isParseSourceSpan(span)) { if (isParseSourceSpan(span)) {
@ -39,8 +42,8 @@ export function spanOf(span?: SpanHolder | ParseSourceSpan): Span|undefined {
} }
export function inSpan(position: number, span?: Span, exclusive?: boolean): boolean { export function inSpan(position: number, span?: Span, exclusive?: boolean): boolean {
return span && exclusive ? position >= span.start && position < span.end : return span != null && (exclusive ? position >= span.start && position < span.end :
position >= span !.start && position <= span !.end; position >= span.start && position <= span.end);
} }
export function offsetSpan(span: Span, amount: number): Span { export function offsetSpan(span: Span, amount: number): Span {
@ -54,7 +57,8 @@ export function isNarrower(spanA: Span, spanB: Span): boolean {
export function hasTemplateReference(type: CompileTypeMetadata): boolean { export function hasTemplateReference(type: CompileTypeMetadata): boolean {
if (type.diDeps) { if (type.diDeps) {
for (let diDep of type.diDeps) { for (let diDep of type.diDeps) {
if (diDep.token !.identifier && identifierName(diDep.token !.identifier !) == 'TemplateRef') if (diDep.token && diDep.token.identifier &&
identifierName(diDep.token !.identifier !) == 'TemplateRef')
return true; return true;
} }
} }