From bb0902c592dedac701e76a471e2ce43e62c62b28 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Tue, 9 May 2017 16:16:50 -0700 Subject: [PATCH] refactor(compiler-cli): move the expression expression type checker (#16562) The expression type checker moved from the language service to the compiler-cli in preparation to using it to check template expressions. --- packages/compiler-cli/index.ts | 6 +- .../src/diagnostics/expression_diagnostics.ts | 325 +++++++ .../src/diagnostics/expression_type.ts | 394 ++++++++ .../compiler-cli/src/diagnostics/symbols.ts | 352 +++++++ .../src/diagnostics/typescript_symbols.ts | 861 ++++++++++++++++++ .../expression_diagnostics_spec.ts | 230 +++++ .../compiler-cli/test/diagnostics/mocks.ts | 255 ++++++ .../src/ast_path.ts | 19 +- packages/compiler/src/compiler.ts | 1 + .../src/compiler_util/expression_converter.ts | 40 +- .../compiler/src/expression_parser/ast.ts | 84 ++ packages/compiler/src/ml_parser/ast.ts | 68 ++ .../src/template_parser/template_ast.ts | 75 ++ packages/language-service/src/completions.ts | 31 +- packages/language-service/src/diagnostics.ts | 188 +--- packages/language-service/src/expressions.ts | 678 +------------- packages/language-service/src/html_path.ts | 72 -- .../language-service/src/language_service.ts | 8 +- .../language-service/src/locate_symbol.ts | 21 +- .../language-service/src/template_path.ts | 151 --- packages/language-service/src/types.ts | 448 +-------- .../language-service/src/typescript_host.ts | 785 +--------------- packages/language-service/src/utils.ts | 69 +- 23 files changed, 2891 insertions(+), 2270 deletions(-) create mode 100644 packages/compiler-cli/src/diagnostics/expression_diagnostics.ts create mode 100644 packages/compiler-cli/src/diagnostics/expression_type.ts create mode 100644 packages/compiler-cli/src/diagnostics/symbols.ts create mode 100644 packages/compiler-cli/src/diagnostics/typescript_symbols.ts create mode 100644 packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts create mode 100644 packages/compiler-cli/test/diagnostics/mocks.ts rename packages/{language-service => compiler}/src/ast_path.ts (60%) delete mode 100644 packages/language-service/src/html_path.ts delete mode 100644 packages/language-service/src/template_path.ts diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index edcf54702e..9cb701fdfb 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -12,6 +12,10 @@ export {Extractor} from './src/extractor'; export * from '@angular/tsc-wrapped'; export {VERSION} from './src/version'; +export {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics, getExpressionScope} from './src/diagnostics/expression_diagnostics'; +export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expression_type'; +export {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './src/diagnostics/typescript_symbols'; +export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './src/diagnostics/symbols'; // TODO(hansl): moving to Angular 4 need to update this API. -export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api' +export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api'; diff --git a/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts b/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts new file mode 100644 index 0000000000..6495b13d5d --- /dev/null +++ b/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts @@ -0,0 +1,325 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AST, AstPath, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveSummary, CompileTypeMetadata, DirectiveAst, ElementAst, EmbeddedTemplateAst, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstPath, VariableAst, findNode, identifierName, templateVisitAll, tokenReference} from '@angular/compiler'; + +import {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './expression_type'; +import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols'; + +export interface DiagnosticTemplateInfo { + fileName?: string; + offset: number; + query: SymbolQuery; + members: SymbolTable; + htmlAst: Node[]; + templateAst: TemplateAst[]; +} + +export interface ExpressionDiagnostic { + message: string; + span: Span; + kind: DiagnosticKind; +} + +export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo): + ExpressionDiagnostic[] { + const visitor = new ExpressionDiagnosticsVisitor( + info, (path: TemplateAstPath, includeEvent: boolean) => + getExpressionScope(info, path, includeEvent)); + templateVisitAll(visitor, info.templateAst); + return visitor.diagnostics; +} + +export function getExpressionDiagnostics( + scope: SymbolTable, ast: AST, query: SymbolQuery, + context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] { + const analyzer = new AstType(scope, query, context); + analyzer.getDiagnostics(ast); + return analyzer.diagnostics; +} + +function getReferences(info: DiagnosticTemplateInfo): SymbolDeclaration[] { + const result: SymbolDeclaration[] = []; + + function processReferences(references: ReferenceAst[]) { + for (const reference of references) { + let type: Symbol|undefined = undefined; + if (reference.value) { + type = info.query.getTypeSymbol(tokenReference(reference.value)); + } + result.push({ + name: reference.name, + kind: 'reference', + type: type || info.query.getBuiltinType(BuiltinType.Any), + get definition() { return getDefintionOf(info, reference); } + }); + } + } + + const visitor = new class extends RecursiveTemplateAstVisitor { + visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { + super.visitEmbeddedTemplate(ast, context); + processReferences(ast.references); + } + visitElement(ast: ElementAst, context: any): any { + super.visitElement(ast, context); + processReferences(ast.references); + } + }; + + templateVisitAll(visitor, info.templateAst); + + return result; +} + +function getDefintionOf(info: DiagnosticTemplateInfo, ast: TemplateAst): Definition|undefined { + if (info.fileName) { + const templateOffset = info.offset; + return [{ + fileName: info.fileName, + span: { + start: ast.sourceSpan.start.offset + templateOffset, + end: ast.sourceSpan.end.offset + templateOffset + } + }]; + } +} + +function getVarDeclarations( + info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration[] { + const result: SymbolDeclaration[] = []; + + let current = path.tail; + while (current) { + if (current instanceof EmbeddedTemplateAst) { + for (const variable of current.variables) { + const name = variable.name; + + // Find the first directive with a context. + const context = + current.directives.map(d => info.query.getTemplateContext(d.directive.type.reference)) + .find(c => !!c); + + // Determine the type of the context field referenced by variable.value. + let type: Symbol|undefined = undefined; + if (context) { + const value = context.get(variable.value); + if (value) { + type = value.type !; + let kind = info.query.getTypeKind(type); + if (kind === BuiltinType.Any || kind == BuiltinType.Unbound) { + // The any type is not very useful here. For special cases, such as ngFor, we can do + // better. + type = refinedVariableType(type, info, current); + } + } + } + if (!type) { + type = info.query.getBuiltinType(BuiltinType.Any); + } + result.push({ + name, + kind: 'variable', type, get definition() { return getDefintionOf(info, variable); } + }); + } + } + current = path.parentOf(current); + } + + return result; +} + +function refinedVariableType( + type: Symbol, info: DiagnosticTemplateInfo, templateElement: EmbeddedTemplateAst): Symbol { + // Special case the ngFor directive + const ngForDirective = templateElement.directives.find(d => { + const name = identifierName(d.directive.type); + return name == 'NgFor' || name == 'NgForOf'; + }); + if (ngForDirective) { + const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf'); + if (ngForOfBinding) { + const bindingType = new AstType(info.members, info.query, {}).getType(ngForOfBinding.value); + if (bindingType) { + const result = info.query.getElementType(bindingType); + if (result) { + return result; + } + } + } + } + + // We can't do better, just return the original type. + return type; +} + +function getEventDeclaration(info: DiagnosticTemplateInfo, includeEvent?: boolean) { + let result: SymbolDeclaration[] = []; + if (includeEvent) { + // TODO: Determine the type of the event parameter based on the Observable or EventEmitter + // of the event. + result = [{name: '$event', kind: 'variable', type: info.query.getBuiltinType(BuiltinType.Any)}]; + } + return result; +} + +export function getExpressionScope( + info: DiagnosticTemplateInfo, path: TemplateAstPath, includeEvent: boolean): SymbolTable { + let result = info.members; + const references = getReferences(info); + const variables = getVarDeclarations(info, path); + const events = getEventDeclaration(info, includeEvent); + if (references.length || variables.length || events.length) { + const referenceTable = info.query.createSymbolTable(references); + const variableTable = info.query.createSymbolTable(variables); + const eventsTable = info.query.createSymbolTable(events); + result = info.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]); + } + return result; +} + +class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor { + private path: TemplateAstPath; + private directiveSummary: CompileDirectiveSummary; + + diagnostics: ExpressionDiagnostic[] = []; + + constructor( + private info: DiagnosticTemplateInfo, + private getExpressionScope: (path: TemplateAstPath, includeEvent: boolean) => SymbolTable) { + super(); + this.path = new AstPath([]); + } + + visitDirective(ast: DirectiveAst, context: any): any { + // Override the default child visitor to ignore the host properties of a directive. + if (ast.inputs && ast.inputs.length) { + templateVisitAll(this, ast.inputs, context); + } + } + + visitBoundText(ast: BoundTextAst): void { + this.push(ast); + this.diagnoseExpression(ast.value, ast.sourceSpan.start.offset, false); + this.pop(); + } + + visitDirectiveProperty(ast: BoundDirectivePropertyAst): void { + this.push(ast); + this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false); + this.pop(); + } + + visitElementProperty(ast: BoundElementPropertyAst): void { + this.push(ast); + this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false); + this.pop(); + } + + visitEvent(ast: BoundEventAst): void { + this.push(ast); + this.diagnoseExpression(ast.handler, this.attributeValueLocation(ast), true); + this.pop(); + } + + visitVariable(ast: VariableAst): void { + const directive = this.directiveSummary; + if (directive && ast.value) { + const context = this.info.query.getTemplateContext(directive.type.reference) !; + if (context && !context.has(ast.value)) { + if (ast.value === '$implicit') { + this.reportError( + 'The template context does not have an implicit value', spanOf(ast.sourceSpan)); + } else { + this.reportError( + `The template context does not defined a member called '${ast.value}'`, + spanOf(ast.sourceSpan)); + } + } + } + } + + visitElement(ast: ElementAst, context: any): void { + this.push(ast); + super.visitElement(ast, context); + this.pop(); + } + + visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { + const previousDirectiveSummary = this.directiveSummary; + + this.push(ast); + + // Find directive that refernces this template + this.directiveSummary = + ast.directives.map(d => d.directive).find(d => hasTemplateReference(d.type)) !; + + // Process children + super.visitEmbeddedTemplate(ast, context); + + this.pop(); + + this.directiveSummary = previousDirectiveSummary; + } + + private attributeValueLocation(ast: TemplateAst) { + const path = findNode(this.info.htmlAst, ast.sourceSpan.start.offset); + const last = path.tail; + if (last instanceof Attribute && last.valueSpan) { + // Add 1 for the quote. + return last.valueSpan.start.offset + 1; + } + return ast.sourceSpan.start.offset; + } + + private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) { + const scope = this.getExpressionScope(this.path, includeEvent); + this.diagnostics.push(...getExpressionDiagnostics(scope, ast, this.info.query, { + event: includeEvent + }).map(d => ({ + span: offsetSpan(d.ast.span, offset + this.info.offset), + kind: d.kind, + message: d.message + }))); + } + + private push(ast: TemplateAst) { this.path.push(ast); } + + private pop() { this.path.pop(); } + + private reportError(message: string, span: Span|undefined) { + if (span) { + this.diagnostics.push( + {span: offsetSpan(span, this.info.offset), kind: DiagnosticKind.Error, message}); + } + } + + private reportWarning(message: string, span: Span) { + this.diagnostics.push( + {span: offsetSpan(span, this.info.offset), kind: DiagnosticKind.Warning, message}); + } +} + +function hasTemplateReference(type: CompileTypeMetadata): boolean { + if (type.diDeps) { + for (let diDep of type.diDeps) { + if (diDep.token && diDep.token.identifier && + identifierName(diDep.token !.identifier !) == 'TemplateRef') + return true; + } + } + return false; +} + +function offsetSpan(span: Span, amount: number): Span { + return {start: span.start + amount, end: span.end + amount}; +} + +function spanOf(sourceSpan: ParseSourceSpan): Span { + return {start: sourceSpan.start.offset, end: sourceSpan.end.offset}; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/diagnostics/expression_type.ts b/packages/compiler-cli/src/diagnostics/expression_type.ts new file mode 100644 index 0000000000..72be38975e --- /dev/null +++ b/packages/compiler-cli/src/diagnostics/expression_type.ts @@ -0,0 +1,394 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, visitAstChildren} from '@angular/compiler'; + +import {BuiltinType, Signature, Span, Symbol, SymbolQuery, SymbolTable} from './symbols'; + +export interface ExpressionDiagnosticsContext { event?: boolean; } + +export enum DiagnosticKind { + Error, + Warning, +} + +export class TypeDiagnostic { + constructor(public kind: DiagnosticKind, public message: string, public ast: AST) {} +} + +// AstType calculatetype of the ast given AST element. +export class AstType implements AstVisitor { + public diagnostics: TypeDiagnostic[]; + + constructor( + private scope: SymbolTable, private query: SymbolQuery, + private context: ExpressionDiagnosticsContext) {} + + getType(ast: AST): Symbol { return ast.visit(this); } + + getDiagnostics(ast: AST): TypeDiagnostic[] { + this.diagnostics = []; + const type: Symbol = ast.visit(this); + if (this.context.event && type.callable) { + this.reportWarning('Unexpected callable expression. Expected a method call', ast); + } + return this.diagnostics; + } + + visitBinary(ast: Binary): Symbol { + // Treat undefined and null as other. + function normalize(kind: BuiltinType, other: BuiltinType): BuiltinType { + switch (kind) { + case BuiltinType.Undefined: + case BuiltinType.Null: + return normalize(other, BuiltinType.Other); + } + return kind; + } + + const leftType = this.getType(ast.left); + const rightType = this.getType(ast.right); + const leftRawKind = this.query.getTypeKind(leftType); + const rightRawKind = this.query.getTypeKind(rightType); + const leftKind = normalize(leftRawKind, rightRawKind); + const rightKind = normalize(rightRawKind, leftRawKind); + + // The following swtich implements operator typing similar to the + // type production tables in the TypeScript specification. + // https://github.com/Microsoft/TypeScript/blob/v1.8.10/doc/spec.md#4.19 + const operKind = leftKind << 8 | rightKind; + switch (ast.operation) { + case '*': + case '/': + case '%': + case '-': + case '<<': + case '>>': + case '>>>': + case '&': + case '^': + case '|': + switch (operKind) { + case BuiltinType.Any << 8 | BuiltinType.Any: + case BuiltinType.Number << 8 | BuiltinType.Any: + case BuiltinType.Any << 8 | BuiltinType.Number: + case BuiltinType.Number << 8 | BuiltinType.Number: + return this.query.getBuiltinType(BuiltinType.Number); + default: + let errorAst = ast.left; + switch (leftKind) { + case BuiltinType.Any: + case BuiltinType.Number: + errorAst = ast.right; + break; + } + return this.reportError('Expected a numeric type', errorAst); + } + case '+': + switch (operKind) { + case BuiltinType.Any << 8 | BuiltinType.Any: + case BuiltinType.Any << 8 | BuiltinType.Boolean: + case BuiltinType.Any << 8 | BuiltinType.Number: + case BuiltinType.Any << 8 | BuiltinType.Other: + case BuiltinType.Boolean << 8 | BuiltinType.Any: + case BuiltinType.Number << 8 | BuiltinType.Any: + case BuiltinType.Other << 8 | BuiltinType.Any: + return this.anyType; + case BuiltinType.Any << 8 | BuiltinType.String: + case BuiltinType.Boolean << 8 | BuiltinType.String: + case BuiltinType.Number << 8 | BuiltinType.String: + case BuiltinType.String << 8 | BuiltinType.Any: + case BuiltinType.String << 8 | BuiltinType.Boolean: + case BuiltinType.String << 8 | BuiltinType.Number: + case BuiltinType.String << 8 | BuiltinType.String: + case BuiltinType.String << 8 | BuiltinType.Other: + case BuiltinType.Other << 8 | BuiltinType.String: + return this.query.getBuiltinType(BuiltinType.String); + case BuiltinType.Number << 8 | BuiltinType.Number: + return this.query.getBuiltinType(BuiltinType.Number); + case BuiltinType.Boolean << 8 | BuiltinType.Number: + case BuiltinType.Other << 8 | BuiltinType.Number: + return this.reportError('Expected a number type', ast.left); + case BuiltinType.Number << 8 | BuiltinType.Boolean: + case BuiltinType.Number << 8 | BuiltinType.Other: + return this.reportError('Expected a number type', ast.right); + default: + return this.reportError('Expected operands to be a string or number type', ast); + } + case '>': + case '<': + case '<=': + case '>=': + case '==': + case '!=': + case '===': + case '!==': + switch (operKind) { + case BuiltinType.Any << 8 | BuiltinType.Any: + case BuiltinType.Any << 8 | BuiltinType.Boolean: + case BuiltinType.Any << 8 | BuiltinType.Number: + case BuiltinType.Any << 8 | BuiltinType.String: + case BuiltinType.Any << 8 | BuiltinType.Other: + case BuiltinType.Boolean << 8 | BuiltinType.Any: + case BuiltinType.Boolean << 8 | BuiltinType.Boolean: + case BuiltinType.Number << 8 | BuiltinType.Any: + case BuiltinType.Number << 8 | BuiltinType.Number: + case BuiltinType.String << 8 | BuiltinType.Any: + case BuiltinType.String << 8 | BuiltinType.String: + case BuiltinType.Other << 8 | BuiltinType.Any: + case BuiltinType.Other << 8 | BuiltinType.Other: + return this.query.getBuiltinType(BuiltinType.Boolean); + default: + return this.reportError('Expected the operants to be of similar type or any', ast); + } + case '&&': + return rightType; + case '||': + return this.query.getTypeUnion(leftType, rightType); + } + + return this.reportError(`Unrecognized operator ${ast.operation}`, ast); + } + + visitChain(ast: Chain) { + if (this.diagnostics) { + // If we are producing diagnostics, visit the children + visitAstChildren(ast, this); + } + // The type of a chain is always undefined. + return this.query.getBuiltinType(BuiltinType.Undefined); + } + + visitConditional(ast: Conditional) { + // The type of a conditional is the union of the true and false conditions. + return this.query.getTypeUnion(this.getType(ast.trueExp), this.getType(ast.falseExp)); + } + + visitFunctionCall(ast: FunctionCall) { + // The type of a function call is the return type of the selected signature. + // The signature is selected based on the types of the arguments. Angular doesn't + // support contextual typing of arguments so this is simpler than TypeScript's + // version. + const args = ast.args.map(arg => this.getType(arg)); + const target = this.getType(ast.target !); + if (!target || !target.callable) return this.reportError('Call target is not callable', ast); + const signature = target.selectSignature(args); + if (signature) return signature.result; + // TODO: Consider a better error message here. + return this.reportError('Unable no compatible signature found for call', ast); + } + + visitImplicitReceiver(ast: ImplicitReceiver): Symbol { + const _this = this; + // Return a pseudo-symbol for the implicit receiver. + // The members of the implicit receiver are what is defined by the + // scope passed into this class. + return { + name: '$implict', + kind: 'component', + language: 'ng-template', + type: undefined, + container: undefined, + callable: false, + nullable: false, + public: true, + definition: undefined, + members(): SymbolTable{return _this.scope;}, + signatures(): Signature[]{return [];}, + selectSignature(types): Signature | undefined{return undefined;}, + indexed(argument): Symbol | undefined{return undefined;} + }; + } + + visitInterpolation(ast: Interpolation): Symbol { + // If we are producing diagnostics, visit the children. + if (this.diagnostics) { + visitAstChildren(ast, this); + } + return this.undefinedType; + } + + visitKeyedRead(ast: KeyedRead): Symbol { + const targetType = this.getType(ast.obj); + const keyType = this.getType(ast.key); + const result = targetType.indexed(keyType); + return result || this.anyType; + } + + visitKeyedWrite(ast: KeyedWrite): Symbol { + // The write of a type is the type of the value being written. + return this.getType(ast.value); + } + + visitLiteralArray(ast: LiteralArray): Symbol { + // A type literal is an array type of the union of the elements + return this.query.getArrayType( + this.query.getTypeUnion(...ast.expressions.map(element => this.getType(element)))); + } + + visitLiteralMap(ast: LiteralMap): Symbol { + // If we are producing diagnostics, visit the children + if (this.diagnostics) { + visitAstChildren(ast, this); + } + // TODO: Return a composite type. + return this.anyType; + } + + visitLiteralPrimitive(ast: LiteralPrimitive) { + // The type of a literal primitive depends on the value of the literal. + switch (ast.value) { + case true: + case false: + return this.query.getBuiltinType(BuiltinType.Boolean); + case null: + return this.query.getBuiltinType(BuiltinType.Null); + case undefined: + return this.query.getBuiltinType(BuiltinType.Undefined); + default: + switch (typeof ast.value) { + case 'string': + return this.query.getBuiltinType(BuiltinType.String); + case 'number': + return this.query.getBuiltinType(BuiltinType.Number); + default: + return this.reportError('Unrecognized primitive', ast); + } + } + } + + visitMethodCall(ast: MethodCall) { + return this.resolveMethodCall(this.getType(ast.receiver), ast); + } + + visitPipe(ast: BindingPipe) { + // The type of a pipe node is the return type of the pipe's transform method. The table returned + // by getPipes() is expected to contain symbols with the corresponding transform method type. + const pipe = this.query.getPipes().get(ast.name); + if (!pipe) return this.reportError(`No pipe by the name ${ast.name} found`, ast); + const expType = this.getType(ast.exp); + const signature = + pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg)))); + if (!signature) return this.reportError('Unable to resolve signature for pipe invocation', ast); + return signature.result; + } + + visitPrefixNot(ast: PrefixNot) { + // The type of a prefix ! is always boolean. + return this.query.getBuiltinType(BuiltinType.Boolean); + } + + visitPropertyRead(ast: PropertyRead) { + return this.resolvePropertyRead(this.getType(ast.receiver), ast); + } + + visitPropertyWrite(ast: PropertyWrite) { + // The type of a write is the type of the value being written. + return this.getType(ast.value); + } + + visitQuote(ast: Quote) { + // The type of a quoted expression is any. + return this.query.getBuiltinType(BuiltinType.Any); + } + + visitSafeMethodCall(ast: SafeMethodCall) { + return this.resolveMethodCall(this.query.getNonNullableType(this.getType(ast.receiver)), ast); + } + + visitSafePropertyRead(ast: SafePropertyRead) { + return this.resolvePropertyRead(this.query.getNonNullableType(this.getType(ast.receiver)), ast); + } + + private _anyType: Symbol; + private get anyType(): Symbol { + let result = this._anyType; + if (!result) { + result = this._anyType = this.query.getBuiltinType(BuiltinType.Any); + } + return result; + } + + private _undefinedType: Symbol; + private get undefinedType(): Symbol { + let result = this._undefinedType; + if (!result) { + result = this._undefinedType = this.query.getBuiltinType(BuiltinType.Undefined); + } + return result; + } + + private resolveMethodCall(receiverType: Symbol, ast: SafeMethodCall|MethodCall) { + if (this.isAny(receiverType)) { + return this.anyType; + } + + // The type of a method is the selected methods result type. + const method = receiverType.members().get(ast.name); + if (!method) return this.reportError(`Unknown method '${ast.name}'`, ast); + if (!method.type) return this.reportError(`Could not find a type for '${ast.name}'`, ast); + 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) + return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast); + return signature.result; + } + + private resolvePropertyRead(receiverType: Symbol, ast: SafePropertyRead|PropertyRead) { + if (this.isAny(receiverType)) { + return this.anyType; + } + + // The type of a property read is the seelcted member's type. + const member = receiverType.members().get(ast.name); + if (!member) { + let receiverInfo = receiverType.name; + if (receiverInfo == '$implict') { + receiverInfo = + 'The component declaration, template variable declarations, and element references do'; + } else if (receiverType.nullable) { + return this.reportError(`The expression might be null`, ast.receiver); + } else { + receiverInfo = `'${receiverInfo}' does`; + } + return this.reportError( + `Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`, + ast); + } + if (!member.public) { + let receiverInfo = receiverType.name; + if (receiverInfo == '$implict') { + receiverInfo = 'the component'; + } else { + receiverInfo = `'${receiverInfo}'`; + } + this.reportWarning( + `Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast); + } + return member.type; + } + + private reportError(message: string, ast: AST): Symbol { + if (this.diagnostics) { + this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Error, message, ast)); + } + return this.anyType; + } + + private reportWarning(message: string, ast: AST): Symbol { + if (this.diagnostics) { + this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Warning, message, ast)); + } + return this.anyType; + } + + private isAny(symbol: Symbol): boolean { + return !symbol || this.query.getTypeKind(symbol) == BuiltinType.Any || + (!!symbol.type && this.isAny(symbol.type)); + } +} \ No newline at end of file diff --git a/packages/compiler-cli/src/diagnostics/symbols.ts b/packages/compiler-cli/src/diagnostics/symbols.ts new file mode 100644 index 0000000000..a0d4be0671 --- /dev/null +++ b/packages/compiler-cli/src/diagnostics/symbols.ts @@ -0,0 +1,352 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {StaticSymbol} from '@angular/compiler'; + +/** + * The range of a span of text in a source file. + * + * @experimental + */ +export interface Span { + /** + * The first code-point of the span as an offset relative to the beginning of the source assuming + * a UTF-16 encoding. + */ + start: number; + + /** + * The first code-point after the span as an offset relative to the beginning of the source + * assuming a UTF-16 encoding. + */ + end: number; +} + +/** + * A file and span. + */ +export interface Location { + fileName: string; + span: Span; +} + +/** + * A defnition location(s). + */ +export type Definition = Location[] | undefined; + +/** + * A symbol describing a language element that can be referenced by expressions + * in an Angular template. + * + * @experimental + */ +export interface Symbol { + /** + * The name of the symbol as it would be referenced in an Angular expression. + */ + readonly name: string; + + /** + * The kind of completion this symbol should generate if included. + */ + readonly kind: string; + + /** + * The language of the source that defines the symbol. (e.g. typescript for TypeScript, + * ng-template for an Angular template, etc.) + */ + readonly language: string; + + /** + * A symbol representing type of the symbol. + */ + readonly type: Symbol|undefined; + + /** + * A symbol for the container of this symbol. For example, if this is a method, the container + * is the class or interface of the method. If no container is appropriate, undefined is + * returned. + */ + readonly container: Symbol|undefined; + + /** + * The symbol is public in the container. + */ + readonly public: boolean; + + /** + * `true` if the symbol can be the target of a call. + */ + readonly callable: boolean; + + /** + * The location of the definition of the symbol + */ + readonly definition: Definition|undefined; + + /** + * `true` if the symbol is a type that is nullable (can be null or undefined). + */ + readonly nullable: boolean; + + /** + * A table of the members of the symbol; that is, the members that can appear + * after a `.` in an Angular expression. + */ + members(): SymbolTable; + + /** + * The list of overloaded signatures that can be used if the symbol is the + * target of a call. + */ + signatures(): Signature[]; + + /** + * Return which signature of returned by `signatures()` would be used selected + * given the `types` supplied. If no signature would match, this method should + * return `undefined`. + */ + selectSignature(types: Symbol[]): Signature|undefined; + + /** + * Return the type of the expression if this symbol is indexed by `argument`. + * If the symbol cannot be indexed, this method should return `undefined`. + */ + indexed(argument: Symbol): Symbol|undefined; +} + +/** + * A table of `Symbol`s accessible by name. + * + * @experimental + */ +export interface SymbolTable { + /** + * The number of symbols in the table. + */ + readonly size: number; + + /** + * Get the symbol corresponding to `key` or `undefined` if there is no symbol in the + * table by the name `key`. + */ + get(key: string): Symbol|undefined; + + /** + * Returns `true` if the table contains a `Symbol` with the name `key`. + */ + has(key: string): boolean; + + /** + * Returns all the `Symbol`s in the table. The order should be, but is not required to be, + * in declaration order. + */ + values(): Symbol[]; +} + +/** + * A description of a function or method signature. + * + * @experimental + */ +export interface Signature { + /** + * The arguments of the signture. The order of `argumetnts.symbols()` must be in the order + * of argument declaration. + */ + readonly arguments: SymbolTable; + + /** + * The symbol of the signature result type. + */ + readonly result: Symbol; +} + +/** + * An enumeration of basic types. + * + * @experimental + */ +export enum BuiltinType { + /** + * The type is a type that can hold any other type. + */ + Any, + + /** + * The type of a string literal. + */ + String, + + /** + * The type of a numeric literal. + */ + Number, + + /** + * The type of the `true` and `false` literals. + */ + Boolean, + + /** + * The type of the `undefined` literal. + */ + Undefined, + + /** + * the type of the `null` literal. + */ + Null, + + /** + * the type is an unbound type parameter. + */ + Unbound, + + /** + * Not a built-in type. + */ + Other +} + +/** + * The kinds of defintion. + * + * @experimental + */ +export type DeclarationKind = 'attribute' | 'html attribute' | 'component' | 'element' | 'entity' | + 'key' | 'method' | 'pipe' | 'property' | 'type' | 'reference' | 'variable'; + +/** + * Describes a symbol to type binding used to build a symbol table. + * + * @experimental + */ +export interface SymbolDeclaration { + /** + * The name of the symbol in table. + */ + readonly name: string; + + /** + * The kind of symbol to declare. + */ + readonly kind: DeclarationKind; + + /** + * Type of the symbol. The type symbol should refer to a symbol for a type. + */ + readonly type: Symbol; + + /** + * The definion of the symbol if one exists. + */ + readonly definition?: Definition; +} + +/** + * Information about the pipes that are available for use in a template. + * + * @experimental + */ +export interface PipeInfo { + /** + * The name of the pipe. + */ + name: string; + + /** + * The static symbol for the pipe's constructor. + */ + symbol: StaticSymbol; +} + +/** + * A sequence of pipe information. + * + * @experimental + */ +export type Pipes = PipeInfo[] | undefined; + +/** + * Describes the language context in which an Angular expression is evaluated. + * + * @experimental + */ +export interface SymbolQuery { + /** + * Return the built-in type this symbol represents or Other if it is not a built-in type. + */ + getTypeKind(symbol: Symbol): BuiltinType; + + /** + * Return a symbol representing the given built-in type. + */ + getBuiltinType(kind: BuiltinType): Symbol; + + /** + * Return the symbol for a type that represents the union of all the types given. Any value + * of one of the types given should be assignable to the returned type. If no one type can + * be constructed then this should be the Any type. + */ + getTypeUnion(...types: Symbol[]): Symbol; + + /** + * Return a symbol for an array type that has the `type` as its element type. + */ + getArrayType(type: Symbol): Symbol; + + /** + * Return element type symbol for an array type if the `type` is an array type. Otherwise return + * undefined. + */ + getElementType(type: Symbol): Symbol|undefined; + + /** + * Return a type that is the non-nullable version of the given type. If `type` is already + * non-nullable, return `type`. + */ + getNonNullableType(type: Symbol): Symbol; + + /** + * Return a symbol table for the pipes that are in scope. + */ + getPipes(): SymbolTable; + + /** + * Return the type symbol for the given static symbol. + */ + getTypeSymbol(type: StaticSymbol): Symbol; + + /** + * Return the members that are in the context of a type's template reference. + */ + getTemplateContext(type: StaticSymbol): SymbolTable|undefined; + + /** + * Produce a symbol table with the given symbols. Used to produce a symbol table + * for use with mergeSymbolTables(). + */ + createSymbolTable(symbols: SymbolDeclaration[]): SymbolTable; + + /** + * Produce a merged symbol table. If the symbol tables contain duplicate entries + * the entries of the latter symbol tables will obscure the entries in the prior + * symbol tables. + * + * The symbol tables passed to this routine MUST be produces by the same instance + * of SymbolQuery that is being called. + */ + mergeSymbolTable(symbolTables: SymbolTable[]): SymbolTable; + + /** + * Return the span of the narrowest non-token node at the given location. + */ + getSpanAt(line: number, column: number): Span|undefined; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/diagnostics/typescript_symbols.ts b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts new file mode 100644 index 0000000000..d53d6d9f5c --- /dev/null +++ b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts @@ -0,0 +1,861 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AotSummaryResolver, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticAndDynamicReflectionCapabilities, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, componentModuleUrl, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; +import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols'; + + +// In TypeScript 2.1 these flags moved +// These helpers work for both 2.0 and 2.1. +const isPrivate = (ts as any).ModifierFlags ? + ((node: ts.Node) => + !!((ts as any).getCombinedModifierFlags(node) & (ts as any).ModifierFlags.Private)) : + ((node: ts.Node) => !!(node.flags & (ts as any).NodeFlags.Private)); + +const isReferenceType = (ts as any).ObjectFlags ? + ((type: ts.Type) => + !!(type.flags & (ts as any).TypeFlags.Object && + (type as any).objectFlags & (ts as any).ObjectFlags.Reference)) : + ((type: ts.Type) => !!(type.flags & (ts as any).TypeFlags.Reference)); + +interface TypeContext { + node: ts.Node; + program: ts.Program; + checker: ts.TypeChecker; +} + +export function getSymbolQuery( + program: ts.Program, checker: ts.TypeChecker, source: ts.SourceFile, + fetchPipes: () => SymbolTable): SymbolQuery { + return new TypeScriptSymbolQuery(program, checker, source, fetchPipes); +} + +export function getClassMembers( + program: ts.Program, checker: ts.TypeChecker, staticSymbol: StaticSymbol): SymbolTable| + undefined { + const declaration = getClassFromStaticSymbol(program, staticSymbol); + if (declaration) { + const type = checker.getTypeAtLocation(declaration); + const node = program.getSourceFile(staticSymbol.filePath); + return new TypeWrapper(type, {node, program, checker}).members(); + } +} + +export function getClassMembersFromDeclaration( + program: ts.Program, checker: ts.TypeChecker, source: ts.SourceFile, + declaration: ts.ClassDeclaration) { + const type = checker.getTypeAtLocation(declaration); + return new TypeWrapper(type, {node: source, program, checker}).members(); +} + +export function getClassFromStaticSymbol( + program: ts.Program, type: StaticSymbol): ts.ClassDeclaration|undefined { + const source = program.getSourceFile(type.filePath); + if (source) { + return ts.forEachChild(source, child => { + if (child.kind === ts.SyntaxKind.ClassDeclaration) { + const classDeclaration = child as ts.ClassDeclaration; + if (classDeclaration.name != null && classDeclaration.name.text === type.name) { + return classDeclaration; + } + } + }) as(ts.ClassDeclaration | undefined); + } + + return undefined; +} + +export function getPipesTable( + source: ts.SourceFile, program: ts.Program, checker: ts.TypeChecker, + pipes: CompilePipeSummary[]): SymbolTable { + return new PipesTable(pipes, {program, checker, node: source}); +} + +class TypeScriptSymbolQuery implements SymbolQuery { + private typeCache = new Map(); + private pipesCache: SymbolTable; + + constructor( + private program: ts.Program, private checker: ts.TypeChecker, private source: ts.SourceFile, + private fetchPipes: () => SymbolTable) {} + + getTypeKind(symbol: Symbol): BuiltinType { return typeKindOf(this.getTsTypeOf(symbol)); } + + getBuiltinType(kind: BuiltinType): Symbol { + let result = this.typeCache.get(kind); + if (!result) { + const type = getBuiltinTypeFromTs( + kind, {checker: this.checker, node: this.source, program: this.program}); + result = + new TypeWrapper(type, {program: this.program, checker: this.checker, node: this.source}); + this.typeCache.set(kind, result); + } + return result; + } + + getTypeUnion(...types: Symbol[]): Symbol { + // No API exists so return any if the types are not all the same type. + let result: Symbol|undefined = undefined; + if (types.length) { + result = types[0]; + for (let i = 1; i < types.length; i++) { + if (types[i] != result) { + result = undefined; + break; + } + } + } + return result || this.getBuiltinType(BuiltinType.Any); + } + + getArrayType(type: Symbol): Symbol { return this.getBuiltinType(BuiltinType.Any); } + + getElementType(type: Symbol): Symbol|undefined { + if (type instanceof TypeWrapper) { + const elementType = getTypeParameterOf(type.tsType, 'Array'); + if (elementType) { + return new TypeWrapper(elementType, type.context); + } + } + } + + getNonNullableType(symbol: Symbol): Symbol { + if (symbol instanceof TypeWrapper && (typeof this.checker.getNonNullableType == 'function')) { + const tsType = symbol.tsType; + const nonNullableType = this.checker.getNonNullableType(tsType); + if (nonNullableType != tsType) { + return new TypeWrapper(nonNullableType, symbol.context); + } else if (nonNullableType == tsType) { + return symbol; + } + } + return this.getBuiltinType(BuiltinType.Any); + } + + getPipes(): SymbolTable { + let result = this.pipesCache; + if (!result) { + result = this.pipesCache = this.fetchPipes(); + } + return result; + } + + getTemplateContext(type: StaticSymbol): SymbolTable|undefined { + const context: TypeContext = {node: this.source, program: this.program, checker: this.checker}; + const typeSymbol = findClassSymbolInContext(type, context); + if (typeSymbol) { + const contextType = this.getTemplateRefContextType(typeSymbol); + if (contextType) return new SymbolWrapper(contextType, context).members(); + } + } + + getTypeSymbol(type: StaticSymbol): Symbol { + const context: TypeContext = {node: this.source, program: this.program, checker: this.checker}; + const typeSymbol = findClassSymbolInContext(type, context) !; + return new SymbolWrapper(typeSymbol, context); + } + + createSymbolTable(symbols: SymbolDeclaration[]): SymbolTable { + const result = new MapSymbolTable(); + result.addAll(symbols.map(s => new DeclaredSymbol(s))); + return result; + } + + mergeSymbolTable(symbolTables: SymbolTable[]): SymbolTable { + const result = new MapSymbolTable(); + for (const symbolTable of symbolTables) { + result.addAll(symbolTable.values()); + } + return result; + } + + getSpanAt(line: number, column: number): Span|undefined { + return spanAt(this.source, line, column); + } + + private getTemplateRefContextType(typeSymbol: ts.Symbol): ts.Symbol|undefined { + const type = this.checker.getTypeOfSymbolAtLocation(typeSymbol, this.source); + const constructor = type.symbol && type.symbol.members && + getFromSymbolTable(type.symbol.members !, '__constructor'); + + if (constructor) { + const constructorDeclaration = constructor.declarations ![0] as ts.ConstructorTypeNode; + for (const parameter of constructorDeclaration.parameters) { + const type = this.checker.getTypeAtLocation(parameter.type !); + if (type.symbol !.name == 'TemplateRef' && isReferenceType(type)) { + const typeReference = type as ts.TypeReference; + if (typeReference.typeArguments.length === 1) { + return typeReference.typeArguments[0].symbol; + } + } + } + } + } + + private getTsTypeOf(symbol: Symbol): ts.Type|undefined { + const type = this.getTypeWrapper(symbol); + return type && type.tsType; + } + + private getTypeWrapper(symbol: Symbol): TypeWrapper|undefined { + let type: TypeWrapper|undefined = undefined; + if (symbol instanceof TypeWrapper) { + type = symbol; + } else if (symbol.type instanceof TypeWrapper) { + type = symbol.type; + } + return type; + } +} + +function typeCallable(type: ts.Type): boolean { + const signatures = type.getCallSignatures(); + return signatures && signatures.length != 0; +} + +function signaturesOf(type: ts.Type, context: TypeContext): Signature[] { + return type.getCallSignatures().map(s => new SignatureWrapper(s, context)); +} + +function selectSignature(type: ts.Type, context: TypeContext, types: Symbol[]): Signature| + undefined { + // TODO: Do a better job of selecting the right signature. + const signatures = type.getCallSignatures(); + return signatures.length ? new SignatureWrapper(signatures[0], context) : undefined; +} + +class TypeWrapper implements Symbol { + constructor(public tsType: ts.Type, public context: TypeContext) { + if (!tsType) { + throw Error('Internal: null type'); + } + } + + get name(): string { + const symbol = this.tsType.symbol; + return (symbol && symbol.name) || ''; + } + + get kind(): DeclarationKind { return 'type'; } + + get language(): string { return 'typescript'; } + + get type(): Symbol|undefined { return undefined; } + + get container(): Symbol|undefined { return undefined; } + + get public(): boolean { return true; } + + get callable(): boolean { return typeCallable(this.tsType); } + + get nullable(): boolean { + return this.context.checker.getNonNullableType(this.tsType) != this.tsType; + } + + get definition(): Definition { return definitionFromTsSymbol(this.tsType.getSymbol()); } + + members(): SymbolTable { + return new SymbolTableWrapper(this.tsType.getProperties(), this.context); + } + + signatures(): Signature[] { return signaturesOf(this.tsType, this.context); } + + selectSignature(types: Symbol[]): Signature|undefined { + return selectSignature(this.tsType, this.context, types); + } + + indexed(argument: Symbol): Symbol|undefined { return undefined; } +} + +class SymbolWrapper implements Symbol { + private symbol: ts.Symbol; + private _tsType: ts.Type; + private _members: SymbolTable; + + constructor(symbol: ts.Symbol, private context: TypeContext) { + this.symbol = symbol && context && (symbol.flags & ts.SymbolFlags.Alias) ? + context.checker.getAliasedSymbol(symbol) : + symbol; + } + + get name(): string { return this.symbol.name; } + + get kind(): DeclarationKind { return this.callable ? 'method' : 'property'; } + + get language(): string { return 'typescript'; } + + get type(): Symbol|undefined { return new TypeWrapper(this.tsType, this.context); } + + get container(): Symbol|undefined { return getContainerOf(this.symbol, this.context); } + + get public(): boolean { + // Symbols that are not explicitly made private are public. + return !isSymbolPrivate(this.symbol); + } + + get callable(): boolean { return typeCallable(this.tsType); } + + get nullable(): boolean { return false; } + + get definition(): Definition { return definitionFromTsSymbol(this.symbol); } + + members(): SymbolTable { + if (!this._members) { + if ((this.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) != 0) { + const declaredType = this.context.checker.getDeclaredTypeOfSymbol(this.symbol); + const typeWrapper = new TypeWrapper(declaredType, this.context); + this._members = typeWrapper.members(); + } else { + this._members = new SymbolTableWrapper(this.symbol.members !, this.context); + } + } + return this._members; + } + + signatures(): Signature[] { return signaturesOf(this.tsType, this.context); } + + selectSignature(types: Symbol[]): Signature|undefined { + return selectSignature(this.tsType, this.context, types); + } + + indexed(argument: Symbol): Symbol|undefined { return undefined; } + + private get tsType(): ts.Type { + let type = this._tsType; + if (!type) { + type = this._tsType = + this.context.checker.getTypeOfSymbolAtLocation(this.symbol, this.context.node); + } + return type; + } +} + +class DeclaredSymbol implements Symbol { + constructor(private declaration: SymbolDeclaration) {} + + get name() { return this.declaration.name; } + + get kind() { return this.declaration.kind; } + + get language(): string { return 'ng-template'; } + + get container(): Symbol|undefined { return undefined; } + + get type() { return this.declaration.type; } + + get callable(): boolean { return this.declaration.type.callable; } + + get nullable(): boolean { return false; } + + get public(): boolean { return true; } + + get definition(): Definition { return this.declaration.definition; } + + members(): SymbolTable { return this.declaration.type.members(); } + + signatures(): Signature[] { return this.declaration.type.signatures(); } + + selectSignature(types: Symbol[]): Signature|undefined { + return this.declaration.type.selectSignature(types); + } + + indexed(argument: Symbol): Symbol|undefined { return undefined; } +} + +class SignatureWrapper implements Signature { + constructor(private signature: ts.Signature, private context: TypeContext) {} + + get arguments(): SymbolTable { + return new SymbolTableWrapper(this.signature.getParameters(), this.context); + } + + get result(): Symbol { return new TypeWrapper(this.signature.getReturnType(), this.context); } +} + +class SignatureResultOverride implements Signature { + constructor(private signature: Signature, private resultType: Symbol) {} + + get arguments(): SymbolTable { return this.signature.arguments; } + + get result(): Symbol { return this.resultType; } +} + +const toSymbolTable: (symbols: ts.Symbol[]) => ts.SymbolTable = isTypescriptVersion('2.2') ? + (symbols => { + const result = new Map(); + for (const symbol of symbols) { + result.set(symbol.name, symbol); + } + return (result as any); + }) : + (symbols => { + const result = {}; + for (const symbol of symbols) { + result[symbol.name] = symbol; + } + return result as ts.SymbolTable; + }); + +function toSymbols(symbolTable: ts.SymbolTable | undefined): ts.Symbol[] { + if (!symbolTable) return []; + + const table = symbolTable as any; + + if (typeof table.values === 'function') { + return Array.from(table.values()) as ts.Symbol[]; + } + + const result: ts.Symbol[] = []; + + const own = typeof table.hasOwnProperty === 'function' ? + (name: string) => table.hasOwnProperty(name) : + (name: string) => !!table[name]; + + for (const name in table) { + if (own(name)) { + result.push(table[name]); + } + } + return result; +} + +class SymbolTableWrapper implements SymbolTable { + private symbols: ts.Symbol[]; + private symbolTable: ts.SymbolTable; + + constructor(symbols: ts.SymbolTable|ts.Symbol[]|undefined, private context: TypeContext) { + symbols = symbols || []; + + if (Array.isArray(symbols)) { + this.symbols = symbols; + this.symbolTable = toSymbolTable(symbols); + } else { + this.symbols = toSymbols(symbols); + this.symbolTable = symbols; + } + } + + get size(): number { return this.symbols.length; } + + get(key: string): Symbol|undefined { + const symbol = getFromSymbolTable(this.symbolTable, key); + return symbol ? new SymbolWrapper(symbol, this.context) : undefined; + } + + has(key: string): boolean { + const table: any = this.symbolTable; + return (typeof table.has === 'function') ? table.has(key) : table[key] != null; + } + + values(): Symbol[] { return this.symbols.map(s => new SymbolWrapper(s, this.context)); } +} + +class MapSymbolTable implements SymbolTable { + private map = new Map(); + private _values: Symbol[] = []; + + get size(): number { return this.map.size; } + + get(key: string): Symbol|undefined { return this.map.get(key); } + + add(symbol: Symbol) { + if (this.map.has(symbol.name)) { + const previous = this.map.get(symbol.name) !; + this._values[this._values.indexOf(previous)] = symbol; + } + this.map.set(symbol.name, symbol); + this._values.push(symbol); + } + + addAll(symbols: Symbol[]) { + for (const symbol of symbols) { + this.add(symbol); + } + } + + has(key: string): boolean { return this.map.has(key); } + + values(): Symbol[] { + // Switch to this.map.values once iterables are supported by the target language. + return this._values; + } +} + +class PipesTable implements SymbolTable { + constructor(private pipes: CompilePipeSummary[], private context: TypeContext) {} + + get size() { return this.pipes.length; } + + get(key: string): Symbol|undefined { + const pipe = this.pipes.find(pipe => pipe.name == key); + if (pipe) { + return new PipeSymbol(pipe, this.context); + } + } + + has(key: string): boolean { return this.pipes.find(pipe => pipe.name == key) != null; } + + values(): Symbol[] { return this.pipes.map(pipe => new PipeSymbol(pipe, this.context)); } +} + +class PipeSymbol implements Symbol { + private _tsType: ts.Type; + + constructor(private pipe: CompilePipeSummary, private context: TypeContext) {} + + get name(): string { return this.pipe.name; } + + get kind(): DeclarationKind { return 'pipe'; } + + get language(): string { return 'typescript'; } + + get type(): Symbol|undefined { return new TypeWrapper(this.tsType, this.context); } + + get container(): Symbol|undefined { return undefined; } + + get callable(): boolean { return true; } + + get nullable(): boolean { return false; } + + get public(): boolean { return true; } + + get definition(): Definition { return definitionFromTsSymbol(this.tsType.getSymbol()); } + + members(): SymbolTable { return EmptyTable.instance; } + + signatures(): Signature[] { return signaturesOf(this.tsType, this.context); } + + selectSignature(types: Symbol[]): Signature|undefined { + let signature = selectSignature(this.tsType, this.context, types) !; + if (types.length == 1) { + const parameterType = types[0]; + if (parameterType instanceof TypeWrapper) { + let resultType: ts.Type|undefined = undefined; + switch (this.name) { + case 'async': + switch (parameterType.name) { + case 'Observable': + case 'Promise': + case 'EventEmitter': + resultType = getTypeParameterOf(parameterType.tsType, parameterType.name); + break; + default: + resultType = getBuiltinTypeFromTs(BuiltinType.Any, this.context); + break; + } + break; + case 'slice': + resultType = getTypeParameterOf(parameterType.tsType, 'Array'); + break; + } + if (resultType) { + signature = new SignatureResultOverride( + signature, new TypeWrapper(resultType, parameterType.context)); + } + } + } + return signature; + } + + indexed(argument: Symbol): Symbol|undefined { return undefined; } + + private get tsType(): ts.Type { + let type = this._tsType; + if (!type) { + const classSymbol = this.findClassSymbol(this.pipe.type.reference); + if (classSymbol) { + type = this._tsType = this.findTransformMethodType(classSymbol) !; + } + if (!type) { + type = this._tsType = getBuiltinTypeFromTs(BuiltinType.Any, this.context); + } + } + return type; + } + + private findClassSymbol(type: StaticSymbol): ts.Symbol|undefined { + return findClassSymbolInContext(type, this.context); + } + + private findTransformMethodType(classSymbol: ts.Symbol): ts.Type|undefined { + const classType = this.context.checker.getDeclaredTypeOfSymbol(classSymbol); + if (classType) { + const transform = classType.getProperty('transform'); + if (transform) { + return this.context.checker.getTypeOfSymbolAtLocation(transform, this.context.node); + } + } + } +} + +function findClassSymbolInContext(type: StaticSymbol, context: TypeContext): ts.Symbol|undefined { + const sourceFile = context.program.getSourceFile(type.filePath); + if (sourceFile) { + const moduleSymbol = (sourceFile as any).module || (sourceFile as any).symbol; + const exports = context.checker.getExportsOfModule(moduleSymbol); + return (exports || []).find(symbol => symbol.name == type.name); + } +} + +class EmptyTable implements SymbolTable { + get size(): number { return 0; } + get(key: string): Symbol|undefined { return undefined; } + has(key: string): boolean { return false; } + values(): Symbol[] { return []; } + static instance = new EmptyTable(); +} + +function findTsConfig(fileName: string): string|undefined { + let dir = path.dirname(fileName); + while (fs.existsSync(dir)) { + const candidate = path.join(dir, 'tsconfig.json'); + if (fs.existsSync(candidate)) return candidate; + const parentDir = path.dirname(dir); + if (parentDir === dir) break; + dir = parentDir; + } +} + +function isBindingPattern(node: ts.Node): node is ts.BindingPattern { + return !!node && (node.kind === ts.SyntaxKind.ArrayBindingPattern || + node.kind === ts.SyntaxKind.ObjectBindingPattern); +} + +function walkUpBindingElementsAndPatterns(node: ts.Node): ts.Node { + while (node && (node.kind === ts.SyntaxKind.BindingElement || isBindingPattern(node))) { + node = node.parent !; + } + + return node; +} + +function getCombinedNodeFlags(node: ts.Node): ts.NodeFlags { + node = walkUpBindingElementsAndPatterns(node); + + let flags = node.flags; + if (node.kind === ts.SyntaxKind.VariableDeclaration) { + node = node.parent !; + } + + if (node && node.kind === ts.SyntaxKind.VariableDeclarationList) { + flags |= node.flags; + node = node.parent !; + } + + if (node && node.kind === ts.SyntaxKind.VariableStatement) { + flags |= node.flags; + } + + return flags; +} + +function isSymbolPrivate(s: ts.Symbol): boolean { + return !!s.valueDeclaration && isPrivate(s.valueDeclaration); +} + +function getBuiltinTypeFromTs(kind: BuiltinType, context: TypeContext): ts.Type { + let type: ts.Type; + const checker = context.checker; + const node = context.node; + switch (kind) { + case BuiltinType.Any: + type = checker.getTypeAtLocation(setParents( + { + kind: ts.SyntaxKind.AsExpression, + expression: {kind: ts.SyntaxKind.TrueKeyword}, + type: {kind: ts.SyntaxKind.AnyKeyword} + }, + node)); + break; + case BuiltinType.Boolean: + type = + checker.getTypeAtLocation(setParents({kind: ts.SyntaxKind.TrueKeyword}, node)); + break; + case BuiltinType.Null: + type = + checker.getTypeAtLocation(setParents({kind: ts.SyntaxKind.NullKeyword}, node)); + break; + case BuiltinType.Number: + const numeric = {kind: ts.SyntaxKind.NumericLiteral}; + setParents({kind: ts.SyntaxKind.ExpressionStatement, expression: numeric}, node); + type = checker.getTypeAtLocation(numeric); + break; + case BuiltinType.String: + type = checker.getTypeAtLocation( + setParents({kind: ts.SyntaxKind.NoSubstitutionTemplateLiteral}, node)); + break; + case BuiltinType.Undefined: + type = checker.getTypeAtLocation(setParents( + { + kind: ts.SyntaxKind.VoidExpression, + expression: {kind: ts.SyntaxKind.NumericLiteral} + }, + node)); + break; + default: + throw new Error(`Internal error, unhandled literal kind ${kind}:${BuiltinType[kind]}`); + } + return type; +} + +function setParents(node: T, parent: ts.Node): T { + node.parent = parent; + ts.forEachChild(node, child => setParents(child, node)); + return node; +} + +function spanOf(node: ts.Node): Span { + return {start: node.getStart(), end: node.getEnd()}; +} + +function shrink(span: Span, offset?: number) { + if (offset == null) offset = 1; + return {start: span.start + offset, end: span.end - offset}; +} + +function spanAt(sourceFile: ts.SourceFile, line: number, column: number): Span|undefined { + if (line != null && column != null) { + const position = ts.getPositionOfLineAndCharacter(sourceFile, line, column); + const findChild = function findChild(node: ts.Node): ts.Node | undefined { + if (node.kind > ts.SyntaxKind.LastToken && node.pos <= position && node.end > position) { + const betterNode = ts.forEachChild(node, findChild); + return betterNode || node; + } + }; + + const node = ts.forEachChild(sourceFile, findChild); + if (node) { + return {start: node.getStart(), end: node.getEnd()}; + } + } +} + +function definitionFromTsSymbol(symbol: ts.Symbol): Definition { + const declarations = symbol.declarations; + if (declarations) { + return declarations.map(declaration => { + const sourceFile = declaration.getSourceFile(); + return { + fileName: sourceFile.fileName, + span: {start: declaration.getStart(), end: declaration.getEnd()} + }; + }); + } +} + +function parentDeclarationOf(node: ts.Node): ts.Node|undefined { + while (node) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + return node; + case ts.SyntaxKind.SourceFile: + return undefined; + } + node = node.parent !; + } +} + +function getContainerOf(symbol: ts.Symbol, context: TypeContext): Symbol|undefined { + if (symbol.getFlags() & ts.SymbolFlags.ClassMember && symbol.declarations) { + for (const declaration of symbol.declarations) { + const parent = parentDeclarationOf(declaration); + if (parent) { + const type = context.checker.getTypeAtLocation(parent); + if (type) { + return new TypeWrapper(type, context); + } + } + } + } +} + +function getTypeParameterOf(type: ts.Type, name: string): ts.Type|undefined { + if (type && type.symbol && type.symbol.name == name) { + const typeArguments: ts.Type[] = (type as any).typeArguments; + if (typeArguments && typeArguments.length <= 1) { + return typeArguments[0]; + } + } +} + +function typeKindOf(type: ts.Type | undefined): BuiltinType { + if (type) { + if (type.flags & ts.TypeFlags.Any) { + return BuiltinType.Any; + } else if ( + type.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLike | ts.TypeFlags.StringLiteral)) { + return BuiltinType.String; + } else if (type.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLike)) { + return BuiltinType.Number; + } else if (type.flags & (ts.TypeFlags.Undefined)) { + return BuiltinType.Undefined; + } else if (type.flags & (ts.TypeFlags.Null)) { + return BuiltinType.Null; + } else if (type.flags & ts.TypeFlags.Union) { + // If all the constituent types of a union are the same kind, it is also that kind. + let candidate: BuiltinType|null = null; + const unionType = type as ts.UnionType; + if (unionType.types.length > 0) { + candidate = typeKindOf(unionType.types[0]); + for (const subType of unionType.types) { + if (candidate != typeKindOf(subType)) { + return BuiltinType.Other; + } + } + } + if (candidate != null) { + return candidate; + } + } else if (type.flags & ts.TypeFlags.TypeParameter) { + return BuiltinType.Unbound; + } + } + return BuiltinType.Other; +} + + + +function getFromSymbolTable(symbolTable: ts.SymbolTable, key: string): ts.Symbol|undefined { + const table = symbolTable as any; + let symbol: ts.Symbol|undefined; + + if (typeof table.get === 'function') { + // TS 2.2 uses a Map + symbol = table.get(key); + } else { + // TS pre-2.2 uses an object + symbol = table[key]; + } + + return symbol; +} + +function toNumbers(value: string | undefined): number[] { + return value ? value.split('.').map(v => +v) : []; +} + +function compareNumbers(a: number[], b: number[]): -1|0|1 { + for (let i = 0; i < a.length && i < b.length; i++) { + if (a[i] > b[i]) return 1; + if (a[i] < b[i]) return -1; + } + return 0; +} + +function isTypescriptVersion(low: string, high?: string): boolean { + const tsNumbers = toNumbers(ts.version); + + return compareNumbers(toNumbers(low), tsNumbers) <= 0 && + compareNumbers(toNumbers(high), tsNumbers) >= 0; +} diff --git a/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts b/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts new file mode 100644 index 0000000000..518e117cfa --- /dev/null +++ b/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts @@ -0,0 +1,230 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {StaticSymbol} from '@angular/compiler'; +import {AngularCompilerOptions, CompilerHost} from '@angular/compiler-cli'; +import * as ts from 'typescript'; + +import {getExpressionDiagnostics, getTemplateExpressionDiagnostics} from '../../src/diagnostics/expression_diagnostics'; +import {Directory} from '../mocks'; + +import {DiagnosticContext, MockLanguageServiceHost, getDiagnosticTemplateInfo} from './mocks'; + +describe('expression diagnostics', () => { + let registry: ts.DocumentRegistry; + let host: MockLanguageServiceHost; + let compilerHost: CompilerHost; + let service: ts.LanguageService; + let context: DiagnosticContext; + let aotHost: CompilerHost; + let type: StaticSymbol; + + beforeAll(() => { + registry = ts.createDocumentRegistry(false, '/src'); + host = new MockLanguageServiceHost(['app/app.component.ts'], FILES, '/src'); + service = ts.createLanguageService(host, registry); + const program = service.getProgram(); + const checker = program.getTypeChecker(); + const options: AngularCompilerOptions = Object.create(host.getCompilationSettings()); + options.genDir = '/dist'; + options.basePath = '/src'; + aotHost = new CompilerHost(program, options, host, {verboseInvalidExpression: true}); + context = new DiagnosticContext(service, program, checker, aotHost); + type = context.getStaticSymbol('app/app.component.ts', 'AppComponent'); + }); + + it('should have no diagnostics in default app', () => { + function messageToString(messageText: string | ts.DiagnosticMessageChain): string { + if (typeof messageText == 'string') { + return messageText; + } else { + if (messageText.next) return messageText.messageText + messageToString(messageText.next); + return messageText.messageText; + } + } + + function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) { + if (diagnostics && diagnostics.length) { + const message = + 'messags: ' + diagnostics.map(d => messageToString(d.messageText)).join('\n'); + expect(message).toEqual(''); + } + } + + expectNoDiagnostics(service.getCompilerOptionsDiagnostics()); + expectNoDiagnostics(service.getSyntacticDiagnostics('app/app.component.ts')); + expectNoDiagnostics(service.getSemanticDiagnostics('app/app.component.ts')); + }); + + + function accept(template: string) { + const info = getDiagnosticTemplateInfo(context, type, 'app/app.component.html', template); + if (info) { + const diagnostics = getTemplateExpressionDiagnostics(info); + if (diagnostics && diagnostics.length) { + const message = diagnostics.map(d => d.message).join('\n '); + throw new Error(`Unexpected diagnostics: ${message}`); + } + } else { + expect(info).toBeDefined(); + } + } + + function reject(template: string, expected: string | RegExp) { + const info = getDiagnosticTemplateInfo(context, type, 'app/app.component.html', template); + if (info) { + const diagnostics = getTemplateExpressionDiagnostics(info); + if (diagnostics && diagnostics.length) { + const messages = diagnostics.map(d => d.message).join('\n '); + expect(messages).toContain(expected); + } else { + throw new Error(`Expected an error containing "${expected} in template "${template}"`); + } + } else { + expect(info).toBeDefined(); + } + } + + it('should accept a simple template', () => accept('App works!')); + it('should accept an interpolation', () => accept('App works: {{person.name.first}}')); + it('should reject misspelled access', + () => reject('{{persson}}', 'Identifier \'persson\' is not defined')); + it('should reject access to private', + () => + reject('{{private_person}}', 'Identifier \'private_person\' refers to a private member')); + it('should accept an *ngIf', () => accept('
{{person.name.first}}
')); + it('should reject *ngIf of misspelled identifier', + () => reject( + '
{{person.name.first}}
', + 'Identifier \'persson\' is not defined')); + it('should accept an *ngFor', () => accept(` +
+ {{p.name.first}} {{p.name.last}} +
+ `)); + it('should reject misspelled field in *ngFor', () => reject( + ` +
+ {{p.names.first}} {{p.name.last}} +
+ `, + 'Identifier \'names\' is not defined')); + it('should accept an async expression', + () => accept('{{(promised_person | async)?.name.first || ""}}')); + it('should reject an async misspelled field', + () => reject( + '{{(promised_person | async)?.nume.first || ""}}', 'Identifier \'nume\' is not defined')); + it('should accept an async *ngFor', () => accept(` +
+ {{p.name.first}} {{p.name.last}} +
+ `)); + it('should reject misspelled field an async *ngFor', () => reject( + ` +
+ {{p.name.first}} {{p.nume.last}} +
+ `, + 'Identifier \'nume\' is not defined')); + it('should reject access to potentially undefined field', + () => reject(`
{{maybe_person.name.first}}`, 'The expression might be null')); + it('should accept a safe accss to an undefined field', + () => accept(`
{{maybe_person?.name.first}}
`)); + it('should accept a # reference', () => accept(` +
+ + + +
+

First name value: {{ first.value }}

+

First name valid: {{ first.valid }}

+

Form value: {{ f.value | json }}

+

Form valid: {{ f.valid }}

+ `)); + it('should reject a misspelled field of a # reference', + () => reject( + ` +
+ + + +
+

First name value: {{ first.valwe }}

+

First name valid: {{ first.valid }}

+

Form value: {{ f.value | json }}

+

Form valid: {{ f.valid }}

+ `, + 'Identifier \'valwe\' is not defined')); + it('should accept a call to a method', () => accept('{{getPerson().name.first}}')); + it('should reject a misspelled field of a method result', + () => reject('{{getPerson().nume.first}}', 'Identifier \'nume\' is not defined')); + it('should reject calling a uncallable member', + () => reject('{{person().name.first}}', 'Member \'person\' is not callable')); + it('should accept an event handler', + () => accept('
{{person.name.first}}
')); + it('should reject a misspelled event handler', + () => reject( + '
{{person.name.first}}
', 'Unknown method \'clack\'')); + it('should reject an uncalled event handler', + () => reject( + '
{{person.name.first}}
', 'Unexpected callable expression')); + +}); + +const FILES: Directory = { + 'src': { + 'app': { + 'app.component.ts': ` + import { Component, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + import { FormsModule } from '@angular/forms'; + + export interface Person { + name: Name; + address: Address; + } + + export interface Name { + first: string; + middle: string; + last: string; + } + + export interface Address { + street: string; + city: string; + state: string; + zip: string; + } + + @Component({ + selector: 'my-app', + templateUrl: './app.component.html' + }) + export class AppComponent { + person: Person; + people: Person[]; + maybe_person?: Person; + promised_person: Promise; + promised_people: Promise; + private private_person: Person; + private private_people: Person[]; + + getPerson(): Person { return this.person; } + click() {} + } + + @NgModule({ + imports: [CommonModule, FormsModule], + declarations: [AppComponent] + }) + export class AppModule {} + ` + } + } +}; \ No newline at end of file diff --git a/packages/compiler-cli/test/diagnostics/mocks.ts b/packages/compiler-cli/test/diagnostics/mocks.ts new file mode 100644 index 0000000000..f269db68d1 --- /dev/null +++ b/packages/compiler-cli/test/diagnostics/mocks.ts @@ -0,0 +1,255 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticAndDynamicReflectionCapabilities, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; +import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; +import {CompilerHostContext} from 'compiler-cli'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +import {DiagnosticTemplateInfo} from '../../src/diagnostics/expression_diagnostics'; +import {getClassFromStaticSymbol, getClassMembers, getPipesTable, getSymbolQuery} from '../../src/diagnostics/typescript_symbols'; +import {Directory, MockAotContext} from '../mocks'; + +const packages = path.join(__dirname, '../../../../../packages'); + +const realFiles = new Map(); + +export class MockLanguageServiceHost implements ts.LanguageServiceHost, CompilerHostContext { + private options: ts.CompilerOptions; + private context: MockAotContext; + private assumedExist = new Set(); + + constructor(private scripts: string[], files: Directory, currentDirectory: string = '/') { + this.options = { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.CommonJS, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + emitDecoratorMetadata: true, + experimentalDecorators: true, + removeComments: false, + noImplicitAny: false, + skipLibCheck: true, + skipDefaultLibCheck: true, + strictNullChecks: true, + baseUrl: currentDirectory, + lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'], + paths: {'@angular/*': [packages + '/*']} + }; + this.context = new MockAotContext(currentDirectory, files) + } + + getCompilationSettings(): ts.CompilerOptions { return this.options; } + + getScriptFileNames(): string[] { return this.scripts; } + + getScriptVersion(fileName: string): string { return '0'; } + + getScriptSnapshot(fileName: string): ts.IScriptSnapshot|undefined { + const content = this.internalReadFile(fileName); + if (content) { + return ts.ScriptSnapshot.fromString(content); + } + } + + getCurrentDirectory(): string { return this.context.currentDirectory; } + + getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; } + + readFile(fileName: string): string { return this.internalReadFile(fileName) as string; } + + readResource(fileName: string): Promise { return Promise.resolve(''); } + + assumeFileExists(fileName: string): void { this.assumedExist.add(fileName); } + + fileExists(fileName: string): boolean { + return this.assumedExist.has(fileName) || this.internalReadFile(fileName) != null; + } + + private internalReadFile(fileName: string): string|undefined { + let basename = path.basename(fileName); + if (/^lib.*\.d\.ts$/.test(basename)) { + let libPath = path.dirname(ts.getDefaultLibFilePath(this.getCompilationSettings())); + fileName = path.join(libPath, basename); + } + if (fileName.startsWith('app/')) { + fileName = path.join(this.context.currentDirectory, fileName); + } + if (this.context.fileExists(fileName)) { + return this.context.readFile(fileName); + } + if (realFiles.has(fileName)) { + return realFiles.get(fileName); + } + if (fs.existsSync(fileName)) { + const content = fs.readFileSync(fileName, 'utf8'); + realFiles.set(fileName, content); + return content; + } + return undefined; + } +} + +const staticSymbolCache = new StaticSymbolCache(); +const summaryResolver = new AotSummaryResolver( + { + loadSummary(filePath: string) { return null; }, + isSourceFile(sourceFilePath: string) { return true; }, + getOutputFileName(sourceFilePath: string) { return sourceFilePath; } + }, + staticSymbolCache); + +export class DiagnosticContext { + _analyzedModules: NgAnalyzedModules; + _staticSymbolResolver: StaticSymbolResolver|undefined; + _reflector: StaticReflector|undefined; + _errors: {e: any, path?: string}[] = []; + _resolver: CompileMetadataResolver|undefined; + _refletor: StaticReflector; + + constructor( + public service: ts.LanguageService, public program: ts.Program, + public checker: ts.TypeChecker, public host: AotCompilerHost) {} + + private collectError(e: any, path?: string) { this._errors.push({e, path}); } + + private get staticSymbolResolver(): StaticSymbolResolver { + let result = this._staticSymbolResolver; + if (!result) { + result = this._staticSymbolResolver = new StaticSymbolResolver( + this.host, staticSymbolCache, summaryResolver, + (e, filePath) => this.collectError(e, filePath)); + } + return result; + } + + get reflector(): StaticReflector { + if (!this._reflector) { + const ssr = this.staticSymbolResolver; + const result = this._reflector = new StaticReflector( + summaryResolver, ssr, [], [], (e, filePath) => this.collectError(e, filePath !)); + StaticAndDynamicReflectionCapabilities.install(result); + this._reflector = result; + return result; + } + return this._reflector; + } + + get resolver(): CompileMetadataResolver { + let result = this._resolver; + if (!result) { + const moduleResolver = new NgModuleResolver(this.reflector); + const directiveResolver = new DirectiveResolver(this.reflector); + const pipeResolver = new PipeResolver(this.reflector); + const elementSchemaRegistry = new DomElementSchemaRegistry(); + const resourceLoader = new class extends ResourceLoader { + get(url: string): Promise { return Promise.resolve(''); } + }; + const urlResolver = createOfflineCompileUrlResolver(); + const htmlParser = new class extends HtmlParser { + parse( + source: string, url: string, parseExpansionForms: boolean = false, + interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): + ParseTreeResult { + return new ParseTreeResult([], []); + } + }; + + // This tracks the CompileConfig in codegen.ts. Currently these options + // are hard-coded. + const config = + new CompilerConfig({defaultEncapsulation: ViewEncapsulation.Emulated, useJit: false}); + const directiveNormalizer = + new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config); + + result = this._resolver = new CompileMetadataResolver( + config, moduleResolver, directiveResolver, pipeResolver, new JitSummaryResolver(), + elementSchemaRegistry, directiveNormalizer, new Console(), staticSymbolCache, + this.reflector, (error, type) => this.collectError(error, type && type.filePath)); + } + return result; + } + + get analyzedModules(): NgAnalyzedModules { + let analyzedModules = this._analyzedModules; + if (!analyzedModules) { + const analyzeHost = {isSourceFile(filePath: string) { return true; }}; + const programSymbols = extractProgramSymbols( + this.staticSymbolResolver, this.program.getSourceFiles().map(sf => sf.fileName), + analyzeHost); + + analyzedModules = this._analyzedModules = + analyzeNgModules(programSymbols, analyzeHost, this.resolver); + } + return analyzedModules; + } + + getStaticSymbol(path: string, name: string): StaticSymbol { + return staticSymbolCache.get(path, name); + } +} + +function compileTemplate(context: DiagnosticContext, type: StaticSymbol, template: string) { + // Compiler the template string. + const resolvedMetadata = context.resolver.getNonNormalizedDirectiveMetadata(type); + const metadata = resolvedMetadata && resolvedMetadata.metadata; + if (metadata) { + const rawHtmlParser = new HtmlParser(); + const htmlParser = new I18NHtmlParser(rawHtmlParser); + const expressionParser = new Parser(new Lexer()); + const config = new CompilerConfig(); + const parser = new TemplateParser( + config, expressionParser, new DomElementSchemaRegistry(), htmlParser, null !, []); + const htmlResult = htmlParser.parse(template, '', true); + const analyzedModules = context.analyzedModules; + // let errors: Diagnostic[]|undefined = undefined; + let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(type); + if (ngModule) { + const resolvedDirectives = ngModule.transitiveModule.directives.map( + d => context.resolver.getNonNormalizedDirectiveMetadata(d.reference)); + const directives = removeMissing(resolvedDirectives).map(d => d.metadata.toSummary()); + const pipes = ngModule.transitiveModule.pipes.map( + p => context.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 + }; + } + } +} + +export function getDiagnosticTemplateInfo( + context: DiagnosticContext, type: StaticSymbol, templateFile: string, + template: string): DiagnosticTemplateInfo|undefined { + const compiledTemplate = compileTemplate(context, type, template); + if (compiledTemplate && compiledTemplate.templateAst) { + const members = getClassMembers(context.program, context.checker, type); + if (members) { + const sourceFile = context.program.getSourceFile(type.filePath); + const query = getSymbolQuery( + context.program, context.checker, sourceFile, + () => + getPipesTable(sourceFile, context.program, context.checker, compiledTemplate.pipes)); + return { + fileName: templateFile, + offset: 0, query, members, + htmlAst: compiledTemplate.htmlAst, + templateAst: compiledTemplate.templateAst + }; + } + } +} + +function removeMissing(values: (T | null | undefined)[]): T[] { + return values.filter(e => !!e) as T[]; +} diff --git a/packages/language-service/src/ast_path.ts b/packages/compiler/src/ast_path.ts similarity index 60% rename from packages/language-service/src/ast_path.ts rename to packages/compiler/src/ast_path.ts index 88e8cf5d96..b899bac985 100644 --- a/packages/language-service/src/ast_path.ts +++ b/packages/compiler/src/ast_path.ts @@ -6,8 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ +/** + * A path is an ordered set of elements. Typically a path is to a + * particular offset in a source file. The head of the list is the top + * most node. The tail is the node that contains the offset directly. + * + * For example, the expresion `a + b + c` might have an ast that looks + * like: + * + + * / \ + * a + + * / \ + * b c + * + * The path to the node at offset 9 would be `['+' at 1-10, '+' at 7-10, + * 'c' at 9-10]` and the path the node at offset 1 would be + * `['+' at 1-10, 'a' at 1-2]`. + */ export class AstPath { - constructor(private path: T[]) {} + constructor(private path: T[], public position: number = -1) {} get empty(): boolean { return !this.path || !this.path.length; } get head(): T|undefined { return this.path[0]; } diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 6ddab6402d..8799eebe4d 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -36,6 +36,7 @@ export * from './aot/static_reflection_capabilities'; export * from './aot/static_symbol'; export * from './aot/static_symbol_resolver'; export * from './aot/summary_resolver'; +export * from './ast_path'; export * from './summary_resolver'; export {JitCompiler} from './jit/compiler'; export * from './jit/compiler_factory'; diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 9c58ab7e02..2847598afd 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -239,7 +239,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { return convertToStatementIfNeeded( mode, new o.BinaryOperatorExpr( - op, this.visit(ast.left, _Mode.Expression), this.visit(ast.right, _Mode.Expression))); + op, this._visit(ast.left, _Mode.Expression), this._visit(ast.right, _Mode.Expression))); } visitChain(ast: cdAst.Chain, mode: _Mode): any { @@ -248,11 +248,11 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } visitConditional(ast: cdAst.Conditional, mode: _Mode): any { - const value: o.Expression = this.visit(ast.condition, _Mode.Expression); + const value: o.Expression = this._visit(ast.condition, _Mode.Expression); return convertToStatementIfNeeded( - mode, - value.conditional( - this.visit(ast.trueExp, _Mode.Expression), this.visit(ast.falseExp, _Mode.Expression))); + mode, value.conditional( + this._visit(ast.trueExp, _Mode.Expression), + this._visit(ast.falseExp, _Mode.Expression))); } visitPipe(ast: cdAst.BindingPipe, mode: _Mode): any { @@ -266,7 +266,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { if (ast instanceof BuiltinFunctionCall) { fnResult = ast.converter(convertedArgs); } else { - fnResult = this.visit(ast.target !, _Mode.Expression).callFn(convertedArgs); + fnResult = this._visit(ast.target !, _Mode.Expression).callFn(convertedArgs); } return convertToStatementIfNeeded(mode, fnResult); } @@ -281,7 +281,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { const args = [o.literal(ast.expressions.length)]; for (let i = 0; i < ast.strings.length - 1; i++) { args.push(o.literal(ast.strings[i])); - args.push(this.visit(ast.expressions[i], _Mode.Expression)); + args.push(this._visit(ast.expressions[i], _Mode.Expression)); } args.push(o.literal(ast.strings[ast.strings.length - 1])); @@ -298,14 +298,14 @@ class _AstToIrVisitor implements cdAst.AstVisitor { return this.convertSafeAccess(ast, leftMostSafe, mode); } else { return convertToStatementIfNeeded( - mode, this.visit(ast.obj, _Mode.Expression).key(this.visit(ast.key, _Mode.Expression))); + mode, this._visit(ast.obj, _Mode.Expression).key(this._visit(ast.key, _Mode.Expression))); } } visitKeyedWrite(ast: cdAst.KeyedWrite, mode: _Mode): any { - const obj: o.Expression = this.visit(ast.obj, _Mode.Expression); - const key: o.Expression = this.visit(ast.key, _Mode.Expression); - const value: o.Expression = this.visit(ast.value, _Mode.Expression); + const obj: o.Expression = this._visit(ast.obj, _Mode.Expression); + const key: o.Expression = this._visit(ast.key, _Mode.Expression); + const value: o.Expression = this._visit(ast.value, _Mode.Expression); return convertToStatementIfNeeded(mode, obj.key(key).set(value)); } @@ -330,7 +330,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } else { const args = this.visitAll(ast.args, _Mode.Expression); let result: any = null; - const receiver = this.visit(ast.receiver, _Mode.Expression); + const receiver = this._visit(ast.receiver, _Mode.Expression); if (receiver === this._implicitReceiver) { const varExpr = this._getLocal(ast.name); if (varExpr) { @@ -345,7 +345,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } visitPrefixNot(ast: cdAst.PrefixNot, mode: _Mode): any { - return convertToStatementIfNeeded(mode, o.not(this.visit(ast.expression, _Mode.Expression))); + return convertToStatementIfNeeded(mode, o.not(this._visit(ast.expression, _Mode.Expression))); } visitPropertyRead(ast: cdAst.PropertyRead, mode: _Mode): any { @@ -354,7 +354,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { return this.convertSafeAccess(ast, leftMostSafe, mode); } else { let result: any = null; - const receiver = this.visit(ast.receiver, _Mode.Expression); + const receiver = this._visit(ast.receiver, _Mode.Expression); if (receiver === this._implicitReceiver) { result = this._getLocal(ast.name); } @@ -366,7 +366,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any { - const receiver: o.Expression = this.visit(ast.receiver, _Mode.Expression); + const receiver: o.Expression = this._visit(ast.receiver, _Mode.Expression); if (receiver === this._implicitReceiver) { const varExpr = this._getLocal(ast.name); if (varExpr) { @@ -374,7 +374,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } } return convertToStatementIfNeeded( - mode, receiver.prop(ast.name).set(this.visit(ast.value, _Mode.Expression))); + mode, receiver.prop(ast.name).set(this._visit(ast.value, _Mode.Expression))); } visitSafePropertyRead(ast: cdAst.SafePropertyRead, mode: _Mode): any { @@ -385,14 +385,14 @@ class _AstToIrVisitor implements cdAst.AstVisitor { return this.convertSafeAccess(ast, this.leftMostSafeNode(ast), mode); } - visitAll(asts: cdAst.AST[], mode: _Mode): any { return asts.map(ast => this.visit(ast, mode)); } + visitAll(asts: cdAst.AST[], mode: _Mode): any { return asts.map(ast => this._visit(ast, mode)); } visitQuote(ast: cdAst.Quote, mode: _Mode): any { throw new Error(`Quotes are not supported for evaluation! Statement: ${ast.uninterpretedExpression} located at ${ast.location}`); } - private visit(ast: cdAst.AST, mode: _Mode): any { + private _visit(ast: cdAst.AST, mode: _Mode): any { const result = this._resultMap.get(ast); if (result) return result; return (this._nodeMap.get(ast) || ast).visit(this, mode); @@ -439,7 +439,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { // Notice that the first guard condition is the left hand of the left most safe access node // which comes in as leftMostSafe to this routine. - let guardedExpression = this.visit(leftMostSafe.receiver, _Mode.Expression); + let guardedExpression = this._visit(leftMostSafe.receiver, _Mode.Expression); let temporary: o.ReadVarExpr = undefined !; if (this.needsTemporary(leftMostSafe.receiver)) { // If the expression has method calls or pipes then we need to save the result into a @@ -468,7 +468,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } // Recursively convert the node now without the guarded member access. - const access = this.visit(ast, _Mode.Expression); + const access = this._visit(ast, _Mode.Expression); // Remove the mapping. This is not strictly required as the converter only traverses each node // once but is safer if the conversion is changed to traverse the nodes more than once. diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 0ac5960ce1..2db3d28504 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -227,6 +227,29 @@ export interface AstVisitor { visitQuote(ast: Quote, context: any): any; visitSafeMethodCall(ast: SafeMethodCall, context: any): any; visitSafePropertyRead(ast: SafePropertyRead, context: any): any; + visit?(ast: AST, context?: any): any; +} + +export class NullAstVisitor { + visitBinary(ast: Binary, context: any): any {} + visitChain(ast: Chain, context: any): any {} + visitConditional(ast: Conditional, context: any): any {} + visitFunctionCall(ast: FunctionCall, context: any): any {} + visitImplicitReceiver(ast: ImplicitReceiver, context: any): any {} + visitInterpolation(ast: Interpolation, context: any): any {} + visitKeyedRead(ast: KeyedRead, context: any): any {} + visitKeyedWrite(ast: KeyedWrite, context: any): any {} + visitLiteralArray(ast: LiteralArray, context: any): any {} + visitLiteralMap(ast: LiteralMap, context: any): any {} + visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any {} + visitMethodCall(ast: MethodCall, context: any): any {} + visitPipe(ast: BindingPipe, context: any): any {} + visitPrefixNot(ast: PrefixNot, context: any): any {} + visitPropertyRead(ast: PropertyRead, context: any): any {} + visitPropertyWrite(ast: PropertyWrite, context: any): any {} + visitQuote(ast: Quote, context: any): any {} + visitSafeMethodCall(ast: SafeMethodCall, context: any): any {} + visitSafePropertyRead(ast: SafePropertyRead, context: any): any {} } export class RecursiveAstVisitor implements AstVisitor { @@ -390,3 +413,64 @@ export class AstTransformer implements AstVisitor { return new Quote(ast.span, ast.prefix, ast.uninterpretedExpression, ast.location); } } + +export function visitAstChildren(ast: AST, visitor: AstVisitor, context?: any) { + function visit(ast: AST) { + visitor.visit && visitor.visit(ast, context) || ast.visit(visitor, context); + } + + function visitAll(asts: T[]) { asts.forEach(visit); } + + ast.visit({ + visitBinary(ast) { + visit(ast.left); + visit(ast.right); + }, + visitChain(ast) { visitAll(ast.expressions); }, + visitConditional(ast) { + visit(ast.condition); + visit(ast.trueExp); + visit(ast.falseExp); + }, + visitFunctionCall(ast) { + if (ast.target) { + visit(ast.target); + } + visitAll(ast.args); + }, + visitImplicitReceiver(ast) {}, + visitInterpolation(ast) { visitAll(ast.expressions); }, + visitKeyedRead(ast) { + visit(ast.obj); + visit(ast.key); + }, + visitKeyedWrite(ast) { + visit(ast.obj); + visit(ast.key); + visit(ast.obj); + }, + visitLiteralArray(ast) { visitAll(ast.expressions); }, + visitLiteralMap(ast) {}, + visitLiteralPrimitive(ast) {}, + visitMethodCall(ast) { + visit(ast.receiver); + visitAll(ast.args); + }, + visitPipe(ast) { + visit(ast.exp); + visitAll(ast.args); + }, + visitPrefixNot(ast) { visit(ast.expression); }, + visitPropertyRead(ast) { visit(ast.receiver); }, + visitPropertyWrite(ast) { + visit(ast.receiver); + visit(ast.value); + }, + visitQuote(ast) {}, + visitSafeMethodCall(ast) { + visit(ast.receiver); + visitAll(ast.args); + }, + visitSafePropertyRead(ast) { visit(ast.receiver); }, + }); +} diff --git a/packages/compiler/src/ml_parser/ast.ts b/packages/compiler/src/ml_parser/ast.ts index 03ebb292da..0a5b0e6449 100644 --- a/packages/compiler/src/ml_parser/ast.ts +++ b/packages/compiler/src/ml_parser/ast.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {AstPath} from '../ast_path'; import {ParseSourceSpan} from '../parse_util'; export interface Node { @@ -80,3 +81,70 @@ export function visitAll(visitor: Visitor, nodes: Node[], context: any = null): }); return result; } + +export class RecursiveVisitor implements Visitor { + constructor() {} + + visitElement(ast: Element, context: any): any { + this.visitChildren(context, visit => { + visit(ast.attrs); + visit(ast.children); + }); + } + + visitAttribute(ast: Attribute, context: any): any {} + visitText(ast: Text, context: any): any {} + visitComment(ast: Comment, context: any): any {} + + visitExpansion(ast: Expansion, context: any): any { + return this.visitChildren(context, visit => { visit(ast.cases); }); + } + + visitExpansionCase(ast: ExpansionCase, context: any): any {} + + private visitChildren( + context: any, cb: (visit: ((children: V[]|undefined) => void)) => void) { + let results: any[][] = []; + let t = this; + function visit(children: T[] | undefined) { + if (children) results.push(visitAll(t, children, context)); + } + cb(visit); + return [].concat.apply([], results); + } +} + +export type HtmlAstPath = AstPath; + +function spanOf(ast: Node) { + const start = ast.sourceSpan.start.offset; + let end = ast.sourceSpan.end.offset; + if (ast instanceof Element) { + if (ast.endSourceSpan) { + end = ast.endSourceSpan.end.offset; + } else if (ast.children && ast.children.length) { + end = spanOf(ast.children[ast.children.length - 1]).end; + } + } + return {start, end}; +} + +export function findNode(nodes: Node[], position: number): HtmlAstPath { + const path: Node[] = []; + + const visitor = new class extends RecursiveVisitor { + visit(ast: Node, context: any): any { + const span = spanOf(ast); + if (span.start <= position && position < span.end) { + path.push(ast); + } else { + // Returning a value here will result in the children being skipped. + return true; + } + } + } + + visitAll(visitor, nodes); + + return new AstPath(path, position); +} \ No newline at end of file diff --git a/packages/compiler/src/template_parser/template_ast.ts b/packages/compiler/src/template_parser/template_ast.ts index 63b526e048..6d3b80e215 100644 --- a/packages/compiler/src/template_parser/template_ast.ts +++ b/packages/compiler/src/template_parser/template_ast.ts @@ -8,10 +8,12 @@ import {SecurityContext, ɵLifecycleHooks as LifecycleHooks} from '@angular/core'; +import {AstPath} from '../ast_path'; import {CompileDirectiveSummary, CompileProviderMetadata, CompileTokenMetadata} from '../compile_metadata'; import {AST} from '../expression_parser/ast'; import {ParseSourceSpan} from '../parse_util'; + /** * An Abstract Syntax Tree node representing part of a parsed Angular template. */ @@ -268,6 +270,77 @@ export interface TemplateAstVisitor { visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any; } +/** + * A visitor that accepts each node but doesn't do anything. It is intended to be used + * as the base class for a visitor that is only interested in a subset of the node types. + */ +export class NullTemplateVisitor implements TemplateAstVisitor { + visitNgContent(ast: NgContentAst, context: any): void {} + visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): void {} + visitElement(ast: ElementAst, context: any): void {} + visitReference(ast: ReferenceAst, context: any): void {} + visitVariable(ast: VariableAst, context: any): void {} + visitEvent(ast: BoundEventAst, context: any): void {} + visitElementProperty(ast: BoundElementPropertyAst, context: any): void {} + visitAttr(ast: AttrAst, context: any): void {} + visitBoundText(ast: BoundTextAst, context: any): void {} + visitText(ast: TextAst, context: any): void {} + visitDirective(ast: DirectiveAst, context: any): void {} + visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): void {} +} + +/** + * Base class that can be used to build a visitor that visits each node + * in an template ast recursively. + */ +export class RecursiveTemplateAstVisitor extends NullTemplateVisitor implements TemplateAstVisitor { + constructor() { super(); } + + // Nodes with children + visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { + return this.visitChildren(context, visit => { + visit(ast.attrs); + visit(ast.references); + visit(ast.variables); + visit(ast.directives); + visit(ast.providers); + visit(ast.children); + }); + } + + visitElement(ast: ElementAst, context: any): any { + return this.visitChildren(context, visit => { + visit(ast.attrs); + visit(ast.inputs); + visit(ast.outputs); + visit(ast.references); + visit(ast.directives); + visit(ast.providers); + visit(ast.children); + }); + } + + visitDirective(ast: DirectiveAst, context: any): any { + return this.visitChildren(context, visit => { + visit(ast.inputs); + visit(ast.hostProperties); + visit(ast.hostEvents); + }); + } + + protected visitChildren( + context: any, + cb: (visit: ((children: V[]|undefined) => void)) => void) { + let results: any[][] = []; + let t = this; + function visit(children: T[] | undefined) { + if (children && children.length) results.push(templateVisitAll(t, children, context)); + } + cb(visit); + return [].concat.apply([], results); + } +} + /** * Visit every node in a list of {@link TemplateAst}s with the given {@link TemplateAstVisitor}. */ @@ -285,3 +358,5 @@ export function templateVisitAll( }); return result; } + +export type TemplateAstPath = AstPath; diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index a60b1febd2..ab78fbce8c 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -6,15 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CssSelector, DirectiveAst, Element, ElementAst, EmbeddedTemplateAst, ImplicitReceiver, NAMED_ENTITIES, NgContentAst, Node as HtmlAst, ParseSpan, PropertyRead, ReferenceAst, SelectorMatcher, TagContentType, TemplateAst, TemplateAstVisitor, Text, TextAst, VariableAst, getHtmlTagDefinition, splitNsName, templateVisitAll} from '@angular/compiler'; +import {AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CssSelector, DirectiveAst, Element, ElementAst, EmbeddedTemplateAst, ImplicitReceiver, NAMED_ENTITIES, NgContentAst, Node as HtmlAst, NullTemplateVisitor, ParseSpan, PropertyRead, ReferenceAst, SelectorMatcher, TagContentType, TemplateAst, TemplateAstVisitor, Text, TextAst, VariableAst, findNode, getHtmlTagDefinition, splitNsName, templateVisitAll} from '@angular/compiler'; +import {DiagnosticTemplateInfo, getExpressionScope} from '@angular/compiler-cli'; import {AstResult, AttrInfo, SelectorInfo, TemplateInfo} from './common'; -import {getExpressionCompletions, getExpressionScope} from './expressions'; +import {getExpressionCompletions} from './expressions'; import {attributeNames, elementNames, eventNames, propertyNames} from './html_info'; -import {HtmlAstPath} from './html_path'; -import {NullTemplateVisitor, TemplateAstChildVisitor, TemplateAstPath} from './template_path'; import {BuiltinType, Completion, Completions, Span, Symbol, SymbolDeclaration, SymbolTable, TemplateSource} from './types'; -import {flatten, getSelectors, hasTemplateReference, inSpan, removeSuffix, spanOf, uniqueByName} from './utils'; +import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, flatten, getSelectors, hasTemplateReference, inSpan, removeSuffix, spanOf, uniqueByName} from './utils'; const TEMPLATE_ATTR_PREFIX = '*'; @@ -35,7 +34,7 @@ export function getTemplateCompletions(templateInfo: TemplateInfo): Completions| // 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 = new HtmlAstPath(htmlAst, templatePosition); + let path = findNode(htmlAst, templatePosition); let mostSpecific = path.tail; if (path.empty || !mostSpecific) { result = elementCompletions(templateInfo, path); @@ -98,7 +97,7 @@ export function getTemplateCompletions(templateInfo: TemplateInfo): Completions| return result; } -function attributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions|undefined { +function attributeCompletions(info: TemplateInfo, 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); @@ -191,18 +190,19 @@ function getAttributeInfosForElement( function attributeValueCompletions( info: TemplateInfo, position: number, attr: Attribute): Completions|undefined { - const path = new TemplateAstPath(info.templateAst, position); + const path = findTemplateAstAt(info.templateAst, position); const mostSpecific = path.tail; + const dinfo = diagnosticInfoFromTemplateInfo(info); if (mostSpecific) { const visitor = - new ExpressionVisitor(info, position, attr, () => getExpressionScope(info, path, false)); + new ExpressionVisitor(info, position, attr, () => getExpressionScope(dinfo, path, false)); mostSpecific.visit(visitor, null); if (!visitor.result || !visitor.result.length) { // Try allwoing widening the path - const widerPath = new TemplateAstPath(info.templateAst, position, /* allowWidening */ true); + const widerPath = findTemplateAstAt(info.templateAst, position, /* allowWidening */ true); if (widerPath.tail) { const widerVisitor = new ExpressionVisitor( - info, position, attr, () => getExpressionScope(info, widerPath, false)); + info, position, attr, () => getExpressionScope(dinfo, widerPath, false)); widerPath.tail.visit(widerVisitor, null); return widerVisitor.result; } @@ -211,7 +211,7 @@ function attributeValueCompletions( } } -function elementCompletions(info: TemplateInfo, path: HtmlAstPath): Completions|undefined { +function elementCompletions(info: TemplateInfo, path: AstPath): Completions|undefined { let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements)); // Collect the elements referenced by the selectors @@ -245,11 +245,12 @@ function entityCompletions(value: string, position: number): Completions|undefin function interpolationCompletions(info: TemplateInfo, position: number): Completions|undefined { // Look for an interpolation in at the position. - const templatePath = new TemplateAstPath(info.templateAst, position); + const templatePath = findTemplateAstAt(info.templateAst, position); const mostSpecific = templatePath.tail; if (mostSpecific) { let visitor = new ExpressionVisitor( - info, position, undefined, () => getExpressionScope(info, templatePath, false)); + info, position, undefined, + () => getExpressionScope(diagnosticInfoFromTemplateInfo(info), templatePath, false)); mostSpecific.visit(visitor, null); return uniqueByName(visitor.result); } @@ -261,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: HtmlAstPath): Completions| +function voidElementAttributeCompletions(info: TemplateInfo, path: AstPath): Completions| undefined { let tail = path.tail; if (tail instanceof Text) { diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index bf380f0b9a..83a37efef3 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -6,14 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveMetadata, CompileDirectiveSummary, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgAnalyzedModules, NgContentAst, ReferenceAst, StaticSymbol, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler'; +import {NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; +import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli'; -import {AstResult, SelectorInfo, TemplateInfo} from './common'; -import {getExpressionDiagnostics, getExpressionScope} from './expressions'; -import {HtmlAstPath} from './html_path'; -import {NullTemplateVisitor, TemplateAstChildVisitor, TemplateAstPath} from './template_path'; -import {Declaration, Declarations, Diagnostic, DiagnosticKind, Diagnostics, Span, SymbolTable, TemplateSource} from './types'; -import {getSelectors, hasTemplateReference, offsetSpan, spanOf} from './utils'; +import {AstResult} from './common'; +import {Declarations, Diagnostic, DiagnosticKind, Diagnostics, Span, TemplateSource} from './types'; +import {offsetSpan, spanOf} from './utils'; export interface AstProvider { getTemplateAst(template: TemplateSource, fileName: string): AstResult; @@ -32,8 +30,15 @@ export function getTemplateDiagnostics( span: offsetSpan(spanOf(e.span), template.span.start), message: e.msg }))); - } else if (ast.templateAst) { - const expressionDiagnostics = getTemplateExpressionDiagnostics(template, ast); + } else if (ast.templateAst && ast.htmlAst) { + const info: DiagnosticTemplateInfo = { + templateAst: ast.templateAst, + htmlAst: ast.htmlAst, + offset: template.span.start, + query: template.query, + members: template.members + }; + const expressionDiagnostics = getTemplateExpressionDiagnostics(info); results.push(...expressionDiagnostics); } if (ast.errors) { @@ -88,168 +93,3 @@ export function getDeclarationDiagnostics( return results; } - -function getTemplateExpressionDiagnostics( - template: TemplateSource, astResult: AstResult): Diagnostics { - if (astResult.htmlAst && astResult.directive && astResult.directives && astResult.pipes && - astResult.templateAst && astResult.expressionParser) { - const info: TemplateInfo = { - template, - htmlAst: astResult.htmlAst, - directive: astResult.directive, - directives: astResult.directives, - pipes: astResult.pipes, - templateAst: astResult.templateAst, - expressionParser: astResult.expressionParser - }; - const visitor = new ExpressionDiagnosticsVisitor( - info, (path: TemplateAstPath, includeEvent: boolean) => - getExpressionScope(info, path, includeEvent)); - templateVisitAll(visitor, astResult.templateAst !); - return visitor.diagnostics; - } - return []; -} - -class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor { - private path: TemplateAstPath; - private directiveSummary: CompileDirectiveSummary; - - diagnostics: Diagnostics = []; - - constructor( - private info: TemplateInfo, - private getExpressionScope: (path: TemplateAstPath, includeEvent: boolean) => SymbolTable) { - super(); - this.path = new TemplateAstPath([], 0); - } - - visitDirective(ast: DirectiveAst, context: any): any { - // Override the default child visitor to ignore the host properties of a directive. - if (ast.inputs && ast.inputs.length) { - templateVisitAll(this, ast.inputs, context); - } - } - - visitBoundText(ast: BoundTextAst): void { - this.push(ast); - this.diagnoseExpression(ast.value, ast.sourceSpan.start.offset, false); - this.pop(); - } - - visitDirectiveProperty(ast: BoundDirectivePropertyAst): void { - this.push(ast); - this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false); - this.pop(); - } - - visitElementProperty(ast: BoundElementPropertyAst): void { - this.push(ast); - this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false); - this.pop(); - } - - visitEvent(ast: BoundEventAst): void { - this.push(ast); - this.diagnoseExpression(ast.handler, this.attributeValueLocation(ast), true); - this.pop(); - } - - visitVariable(ast: VariableAst): void { - const directive = this.directiveSummary; - if (directive && ast.value) { - const context = this.info.template.query.getTemplateContext(directive.type.reference) !; - if (context && !context.has(ast.value)) { - if (ast.value === '$implicit') { - this.reportError( - 'The template context does not have an implicit value', spanOf(ast.sourceSpan)); - } else { - this.reportError( - `The template context does not defined a member called '${ast.value}'`, - spanOf(ast.sourceSpan)); - } - } - } - } - - visitElement(ast: ElementAst, context: any): void { - this.push(ast); - super.visitElement(ast, context); - this.pop(); - } - - visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { - const previousDirectiveSummary = this.directiveSummary; - - this.push(ast); - - // Find directive that refernces this template - this.directiveSummary = - ast.directives.map(d => d.directive).find(d => hasTemplateReference(d.type)) !; - - // Process children - super.visitEmbeddedTemplate(ast, context); - - this.pop(); - - this.directiveSummary = previousDirectiveSummary; - } - - private attributeValueLocation(ast: TemplateAst) { - const path = new HtmlAstPath(this.info.htmlAst, ast.sourceSpan.start.offset); - const last = path.tail; - if (last instanceof Attribute && last.valueSpan) { - // Add 1 for the quote. - return last.valueSpan.start.offset + 1; - } - return ast.sourceSpan.start.offset; - } - - private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) { - const scope = this.getExpressionScope(this.path, includeEvent); - this.diagnostics.push( - ...getExpressionDiagnostics(scope, ast, this.info.template.query, { - event: includeEvent - }).map(d => ({ - span: offsetSpan(d.ast.span, offset + this.info.template.span.start), - kind: d.kind, - message: d.message - }))); - } - - private push(ast: TemplateAst) { this.path.push(ast); } - - private pop() { this.path.pop(); } - - private _selectors: SelectorInfo; - private selectors(): SelectorInfo { - let result = this._selectors; - if (!result) { - this._selectors = result = getSelectors(this.info); - } - return result; - } - - private findElement(position: number): Element|undefined { - const htmlPath = new HtmlAstPath(this.info.htmlAst, position); - if (htmlPath.tail instanceof Element) { - return htmlPath.tail; - } - } - - private reportError(message: string, span: Span|undefined) { - if (span) { - this.diagnostics.push({ - span: offsetSpan(span, this.info.template.span.start), - kind: DiagnosticKind.Error, message - }); - } - } - - private reportWarning(message: string, span: Span) { - this.diagnostics.push({ - span: offsetSpan(span, this.info.template.span.start), - kind: DiagnosticKind.Warning, message - }); - } -} diff --git a/packages/language-service/src/expressions.ts b/packages/language-service/src/expressions.ts index ba3e585bea..2f3b24e8df 100644 --- a/packages/language-service/src/expressions.ts +++ b/packages/language-service/src/expressions.ts @@ -6,27 +6,39 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, ElementAst, EmbeddedTemplateAst, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, ReferenceAst, SafeMethodCall, SafePropertyRead, StaticSymbol, TemplateAst, identifierName, templateVisitAll, tokenReference} from '@angular/compiler'; +import {AST, ASTWithSource, AstPath as AstPathBase, NullAstVisitor, visitAstChildren} from '@angular/compiler'; +import {AstType} from '@angular/compiler-cli'; -import {AstPath as AstPathBase} from './ast_path'; -import {TemplateInfo} from './common'; -import {TemplateAstChildVisitor, TemplateAstPath} from './template_path'; -import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types'; -import {inSpan, spanOf} from './utils'; +import {BuiltinType, Span, Symbol, SymbolQuery, SymbolTable} from './types'; +import {inSpan} from './utils'; -export interface ExpressionDiagnosticsContext { event?: boolean; } +type AstPath = AstPathBase; -export function getExpressionDiagnostics( - scope: SymbolTable, ast: AST, query: SymbolQuery, - context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] { - const analyzer = new AstType(scope, query, context); - analyzer.getDiagnostics(ast); - return analyzer.diagnostics; +function findAstAt(ast: AST, position: number, excludeEmpty: boolean = false): AstPath { + const path: AST[] = []; + const visitor = new class extends NullAstVisitor { + visit(ast: AST) { + if ((!excludeEmpty || ast.span.start < ast.span.end) && inSpan(position, ast.span)) { + path.push(ast); + visitAstChildren(ast, this); + } + } + }; + + // We never care about the ASTWithSource node and its visit() method calls its ast's visit so + // the visit() method above would never see it. + if (ast instanceof ASTWithSource) { + ast = ast.ast; + } + + visitor.visit(ast); + + return new AstPathBase(path, position); } export function getExpressionCompletions( scope: SymbolTable, ast: AST, position: number, query: SymbolQuery): Symbol[]|undefined { - const path = new AstPath(ast, position); + const path = findAstAt(ast, position); if (path.empty) return undefined; const tail = path.tail !; let result: SymbolTable|undefined = scope; @@ -85,7 +97,7 @@ export function getExpressionCompletions( export function getExpressionSymbol( scope: SymbolTable, ast: AST, position: number, query: SymbolQuery): {symbol: Symbol, span: Span}|undefined { - const path = new AstPath(ast, position, /* excludeEmpty */ true); + const path = findAstAt(ast, position, /* excludeEmpty */ true); if (path.empty) return undefined; const tail = path.tail !; @@ -153,639 +165,3 @@ export function getExpressionSymbol( return {symbol, span}; } } - -interface ExpressionVisitor extends AstVisitor { - visit?(ast: AST, context?: any): any; -} - - -// Consider moving to expression_parser/ast -class NullVisitor implements ExpressionVisitor { - visitBinary(ast: Binary): void {} - visitChain(ast: Chain): void {} - visitConditional(ast: Conditional): void {} - visitFunctionCall(ast: FunctionCall): void {} - visitImplicitReceiver(ast: ImplicitReceiver): void {} - visitInterpolation(ast: Interpolation): void {} - visitKeyedRead(ast: KeyedRead): void {} - visitKeyedWrite(ast: KeyedWrite): void {} - visitLiteralArray(ast: LiteralArray): void {} - visitLiteralMap(ast: LiteralMap): void {} - visitLiteralPrimitive(ast: LiteralPrimitive): void {} - visitMethodCall(ast: MethodCall): void {} - visitPipe(ast: BindingPipe): void {} - visitPrefixNot(ast: PrefixNot): void {} - visitPropertyRead(ast: PropertyRead): void {} - visitPropertyWrite(ast: PropertyWrite): void {} - visitQuote(ast: Quote): void {} - visitSafeMethodCall(ast: SafeMethodCall): void {} - visitSafePropertyRead(ast: SafePropertyRead): void {} -} - -export class TypeDiagnostic { - constructor(public kind: DiagnosticKind, public message: string, public ast: AST) {} -} - -// AstType calculatetype of the ast given AST element. -class AstType implements ExpressionVisitor { - public diagnostics: TypeDiagnostic[]; - - constructor( - private scope: SymbolTable, private query: SymbolQuery, - private context: ExpressionDiagnosticsContext) {} - - getType(ast: AST): Symbol { return ast.visit(this); } - - getDiagnostics(ast: AST): TypeDiagnostic[] { - this.diagnostics = []; - const type: Symbol = ast.visit(this); - if (this.context.event && type.callable) { - this.reportWarning('Unexpected callable expression. Expected a method call', ast); - } - return this.diagnostics; - } - - visitBinary(ast: Binary): Symbol { - // Treat undefined and null as other. - function normalize(kind: BuiltinType, other: BuiltinType): BuiltinType { - switch (kind) { - case BuiltinType.Undefined: - case BuiltinType.Null: - return normalize(other, BuiltinType.Other); - } - return kind; - } - - const leftType = this.getType(ast.left); - const rightType = this.getType(ast.right); - const leftRawKind = this.query.getTypeKind(leftType); - const rightRawKind = this.query.getTypeKind(rightType); - const leftKind = normalize(leftRawKind, rightRawKind); - const rightKind = normalize(rightRawKind, leftRawKind); - - // The following swtich implements operator typing similar to the - // type production tables in the TypeScript specification. - // https://github.com/Microsoft/TypeScript/blob/v1.8.10/doc/spec.md#4.19 - const operKind = leftKind << 8 | rightKind; - switch (ast.operation) { - case '*': - case '/': - case '%': - case '-': - case '<<': - case '>>': - case '>>>': - case '&': - case '^': - case '|': - switch (operKind) { - case BuiltinType.Any << 8 | BuiltinType.Any: - case BuiltinType.Number << 8 | BuiltinType.Any: - case BuiltinType.Any << 8 | BuiltinType.Number: - case BuiltinType.Number << 8 | BuiltinType.Number: - return this.query.getBuiltinType(BuiltinType.Number); - default: - let errorAst = ast.left; - switch (leftKind) { - case BuiltinType.Any: - case BuiltinType.Number: - errorAst = ast.right; - break; - } - return this.reportError('Expected a numeric type', errorAst); - } - case '+': - switch (operKind) { - case BuiltinType.Any << 8 | BuiltinType.Any: - case BuiltinType.Any << 8 | BuiltinType.Boolean: - case BuiltinType.Any << 8 | BuiltinType.Number: - case BuiltinType.Any << 8 | BuiltinType.Other: - case BuiltinType.Boolean << 8 | BuiltinType.Any: - case BuiltinType.Number << 8 | BuiltinType.Any: - case BuiltinType.Other << 8 | BuiltinType.Any: - return this.anyType; - case BuiltinType.Any << 8 | BuiltinType.String: - case BuiltinType.Boolean << 8 | BuiltinType.String: - case BuiltinType.Number << 8 | BuiltinType.String: - case BuiltinType.String << 8 | BuiltinType.Any: - case BuiltinType.String << 8 | BuiltinType.Boolean: - case BuiltinType.String << 8 | BuiltinType.Number: - case BuiltinType.String << 8 | BuiltinType.String: - case BuiltinType.String << 8 | BuiltinType.Other: - case BuiltinType.Other << 8 | BuiltinType.String: - return this.query.getBuiltinType(BuiltinType.String); - case BuiltinType.Number << 8 | BuiltinType.Number: - return this.query.getBuiltinType(BuiltinType.Number); - case BuiltinType.Boolean << 8 | BuiltinType.Number: - case BuiltinType.Other << 8 | BuiltinType.Number: - return this.reportError('Expected a number type', ast.left); - case BuiltinType.Number << 8 | BuiltinType.Boolean: - case BuiltinType.Number << 8 | BuiltinType.Other: - return this.reportError('Expected a number type', ast.right); - default: - return this.reportError('Expected operands to be a string or number type', ast); - } - case '>': - case '<': - case '<=': - case '>=': - case '==': - case '!=': - case '===': - case '!==': - switch (operKind) { - case BuiltinType.Any << 8 | BuiltinType.Any: - case BuiltinType.Any << 8 | BuiltinType.Boolean: - case BuiltinType.Any << 8 | BuiltinType.Number: - case BuiltinType.Any << 8 | BuiltinType.String: - case BuiltinType.Any << 8 | BuiltinType.Other: - case BuiltinType.Boolean << 8 | BuiltinType.Any: - case BuiltinType.Boolean << 8 | BuiltinType.Boolean: - case BuiltinType.Number << 8 | BuiltinType.Any: - case BuiltinType.Number << 8 | BuiltinType.Number: - case BuiltinType.String << 8 | BuiltinType.Any: - case BuiltinType.String << 8 | BuiltinType.String: - case BuiltinType.Other << 8 | BuiltinType.Any: - case BuiltinType.Other << 8 | BuiltinType.Other: - return this.query.getBuiltinType(BuiltinType.Boolean); - default: - return this.reportError('Expected the operants to be of similar type or any', ast); - } - case '&&': - return rightType; - case '||': - return this.query.getTypeUnion(leftType, rightType); - } - - return this.reportError(`Unrecognized operator ${ast.operation}`, ast); - } - - visitChain(ast: Chain) { - if (this.diagnostics) { - // If we are producing diagnostics, visit the children - visitChildren(ast, this); - } - // The type of a chain is always undefined. - return this.query.getBuiltinType(BuiltinType.Undefined); - } - - visitConditional(ast: Conditional) { - // The type of a conditional is the union of the true and false conditions. - return this.query.getTypeUnion(this.getType(ast.trueExp), this.getType(ast.falseExp)); - } - - visitFunctionCall(ast: FunctionCall) { - // The type of a function call is the return type of the selected signature. - // The signature is selected based on the types of the arguments. Angular doesn't - // support contextual typing of arguments so this is simpler than TypeScript's - // version. - const args = ast.args.map(arg => this.getType(arg)); - const target = this.getType(ast.target !); - if (!target || !target.callable) return this.reportError('Call target is not callable', ast); - const signature = target.selectSignature(args); - if (signature) return signature.result; - // TODO: Consider a better error message here. - return this.reportError('Unable no compatible signature found for call', ast); - } - - visitImplicitReceiver(ast: ImplicitReceiver): Symbol { - const _this = this; - // Return a pseudo-symbol for the implicit receiver. - // The members of the implicit receiver are what is defined by the - // scope passed into this class. - return { - name: '$implict', - kind: 'component', - language: 'ng-template', - type: undefined, - container: undefined, - callable: false, - public: true, - definition: undefined, - members(): SymbolTable{return _this.scope;}, - signatures(): Signature[]{return [];}, - selectSignature(types): Signature | undefined{return undefined;}, - indexed(argument): Symbol | undefined{return undefined;} - }; - } - - visitInterpolation(ast: Interpolation): Symbol { - // If we are producing diagnostics, visit the children. - if (this.diagnostics) { - visitChildren(ast, this); - } - return this.undefinedType; - } - - visitKeyedRead(ast: KeyedRead): Symbol { - const targetType = this.getType(ast.obj); - const keyType = this.getType(ast.key); - const result = targetType.indexed(keyType); - return result || this.anyType; - } - - visitKeyedWrite(ast: KeyedWrite): Symbol { - // The write of a type is the type of the value being written. - return this.getType(ast.value); - } - - visitLiteralArray(ast: LiteralArray): Symbol { - // A type literal is an array type of the union of the elements - return this.query.getArrayType( - this.query.getTypeUnion(...ast.expressions.map(element => this.getType(element)))); - } - - visitLiteralMap(ast: LiteralMap): Symbol { - // If we are producing diagnostics, visit the children - if (this.diagnostics) { - visitChildren(ast, this); - } - // TODO: Return a composite type. - return this.anyType; - } - - visitLiteralPrimitive(ast: LiteralPrimitive) { - // The type of a literal primitive depends on the value of the literal. - switch (ast.value) { - case true: - case false: - return this.query.getBuiltinType(BuiltinType.Boolean); - case null: - return this.query.getBuiltinType(BuiltinType.Null); - case undefined: - return this.query.getBuiltinType(BuiltinType.Undefined); - default: - switch (typeof ast.value) { - case 'string': - return this.query.getBuiltinType(BuiltinType.String); - case 'number': - return this.query.getBuiltinType(BuiltinType.Number); - default: - return this.reportError('Unrecognized primitive', ast); - } - } - } - - visitMethodCall(ast: MethodCall) { - return this.resolveMethodCall(this.getType(ast.receiver), ast); - } - - visitPipe(ast: BindingPipe) { - // The type of a pipe node is the return type of the pipe's transform method. The table returned - // by getPipes() is expected to contain symbols with the corresponding transform method type. - const pipe = this.query.getPipes().get(ast.name); - if (!pipe) return this.reportError(`No pipe by the name ${ast.name} found`, ast); - const expType = this.getType(ast.exp); - const signature = - pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg)))); - if (!signature) return this.reportError('Unable to resolve signature for pipe invocation', ast); - return signature.result; - } - - visitPrefixNot(ast: PrefixNot) { - // The type of a prefix ! is always boolean. - return this.query.getBuiltinType(BuiltinType.Boolean); - } - - visitPropertyRead(ast: PropertyRead) { - return this.resolvePropertyRead(this.getType(ast.receiver), ast); - } - - visitPropertyWrite(ast: PropertyWrite) { - // The type of a write is the type of the value being written. - return this.getType(ast.value); - } - - visitQuote(ast: Quote) { - // The type of a quoted expression is any. - return this.query.getBuiltinType(BuiltinType.Any); - } - - visitSafeMethodCall(ast: SafeMethodCall) { - return this.resolveMethodCall(this.query.getNonNullableType(this.getType(ast.receiver)), ast); - } - - visitSafePropertyRead(ast: SafePropertyRead) { - return this.resolvePropertyRead(this.query.getNonNullableType(this.getType(ast.receiver)), ast); - } - - private _anyType: Symbol; - private get anyType(): Symbol { - let result = this._anyType; - if (!result) { - result = this._anyType = this.query.getBuiltinType(BuiltinType.Any); - } - return result; - } - - private _undefinedType: Symbol; - private get undefinedType(): Symbol { - let result = this._undefinedType; - if (!result) { - result = this._undefinedType = this.query.getBuiltinType(BuiltinType.Undefined); - } - return result; - } - - private resolveMethodCall(receiverType: Symbol, ast: SafeMethodCall|MethodCall) { - if (this.isAny(receiverType)) { - return this.anyType; - } - - // The type of a method is the selected methods result type. - const method = receiverType.members().get(ast.name); - if (!method) return this.reportError(`Unknown method ${ast.name}`, ast); - if (!method.type) return this.reportError(`Could not find a type for ${ast.name}`, ast); - 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) - return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast); - return signature.result; - } - - private resolvePropertyRead(receiverType: Symbol, ast: SafePropertyRead|PropertyRead) { - if (this.isAny(receiverType)) { - return this.anyType; - } - - // The type of a property read is the seelcted member's type. - const member = receiverType.members().get(ast.name); - if (!member) { - let receiverInfo = receiverType.name; - if (receiverInfo == '$implict') { - receiverInfo = - 'The component declaration, template variable declarations, and element references do'; - } else { - receiverInfo = `'${receiverInfo}' does`; - } - return this.reportError( - `Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`, - ast); - } - if (!member.public) { - let receiverInfo = receiverType.name; - if (receiverInfo == '$implict') { - receiverInfo = 'the component'; - } else { - receiverInfo = `'${receiverInfo}'`; - } - this.reportWarning( - `Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast); - } - return member.type; - } - - private reportError(message: string, ast: AST): Symbol { - if (this.diagnostics) { - this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Error, message, ast)); - } - return this.anyType; - } - - private reportWarning(message: string, ast: AST): Symbol { - if (this.diagnostics) { - this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Warning, message, ast)); - } - return this.anyType; - } - - private isAny(symbol: Symbol): boolean { - return !symbol || this.query.getTypeKind(symbol) == BuiltinType.Any || - (!!symbol.type && this.isAny(symbol.type)); - } -} - -class AstPath extends AstPathBase { - constructor(ast: AST, public position: number, excludeEmpty: boolean = false) { - super(new AstPathVisitor(position, excludeEmpty).buildPath(ast).path); - } -} - -class AstPathVisitor extends NullVisitor { - public path: AST[] = []; - - constructor(private position: number, private excludeEmpty: boolean) { super(); } - - visit(ast: AST) { - if ((!this.excludeEmpty || ast.span.start < ast.span.end) && inSpan(this.position, ast.span)) { - this.path.push(ast); - visitChildren(ast, this); - } - } - - buildPath(ast: AST): AstPathVisitor { - // We never care about the ASTWithSource node and its visit() method calls its ast's visit so - // the visit() method above would never see it. - if (ast instanceof ASTWithSource) { - ast = ast.ast; - } - this.visit(ast); - return this; - } -} - -// TODO: Consider moving to expression_parser/ast -function visitChildren(ast: AST, visitor: ExpressionVisitor) { - function visit(ast: AST) { visitor.visit && visitor.visit(ast) || ast.visit(visitor); } - - function visitAll(asts: T[]) { asts.forEach(visit); } - - ast.visit({ - visitBinary(ast) { - visit(ast.left); - visit(ast.right); - }, - visitChain(ast) { visitAll(ast.expressions); }, - visitConditional(ast) { - visit(ast.condition); - visit(ast.trueExp); - visit(ast.falseExp); - }, - visitFunctionCall(ast) { - if (ast.target) { - visit(ast.target); - } - visitAll(ast.args); - }, - visitImplicitReceiver(ast) {}, - visitInterpolation(ast) { visitAll(ast.expressions); }, - visitKeyedRead(ast) { - visit(ast.obj); - visit(ast.key); - }, - visitKeyedWrite(ast) { - visit(ast.obj); - visit(ast.key); - visit(ast.obj); - }, - visitLiteralArray(ast) { visitAll(ast.expressions); }, - visitLiteralMap(ast) {}, - visitLiteralPrimitive(ast) {}, - visitMethodCall(ast) { - visit(ast.receiver); - visitAll(ast.args); - }, - visitPipe(ast) { - visit(ast.exp); - visitAll(ast.args); - }, - visitPrefixNot(ast) { visit(ast.expression); }, - visitPropertyRead(ast) { visit(ast.receiver); }, - visitPropertyWrite(ast) { - visit(ast.receiver); - visit(ast.value); - }, - visitQuote(ast) {}, - visitSafeMethodCall(ast) { - visit(ast.receiver); - visitAll(ast.args); - }, - visitSafePropertyRead(ast) { visit(ast.receiver); }, - }); -} - -export function getExpressionScope( - info: TemplateInfo, path: TemplateAstPath, includeEvent: boolean): SymbolTable { - let result = info.template.members; - const references = getReferences(info); - const variables = getVarDeclarations(info, path); - const events = getEventDeclaration(info, path, includeEvent); - if (references.length || variables.length || events.length) { - const referenceTable = info.template.query.createSymbolTable(references); - const variableTable = info.template.query.createSymbolTable(variables); - const eventsTable = info.template.query.createSymbolTable(events); - result = - info.template.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]); - } - return result; -} - -function getEventDeclaration(info: TemplateInfo, path: TemplateAstPath, includeEvent?: boolean) { - let result: SymbolDeclaration[] = []; - if (includeEvent) { - // TODO: Determine the type of the event parameter based on the Observable or EventEmitter - // of the event. - result = [{ - name: '$event', - kind: 'variable', - type: info.template.query.getBuiltinType(BuiltinType.Any) - }]; - } - return result; -} - -function getReferences(info: TemplateInfo): SymbolDeclaration[] { - const result: SymbolDeclaration[] = []; - - function processReferences(references: ReferenceAst[]) { - for (const reference of references) { - let type: Symbol|undefined = undefined; - if (reference.value) { - type = info.template.query.getTypeSymbol(tokenReference(reference.value)); - } - result.push({ - name: reference.name, - kind: 'reference', - type: type || info.template.query.getBuiltinType(BuiltinType.Any), - get definition() { return getDefintionOf(info, reference); } - }); - } - } - - const visitor = new class extends TemplateAstChildVisitor { - visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { - super.visitEmbeddedTemplate(ast, context); - processReferences(ast.references); - } - visitElement(ast: ElementAst, context: any): any { - super.visitElement(ast, context); - processReferences(ast.references); - } - }; - - templateVisitAll(visitor, info.templateAst); - - return result; -} - -function getVarDeclarations(info: TemplateInfo, path: TemplateAstPath): SymbolDeclaration[] { - const result: SymbolDeclaration[] = []; - - let current = path.tail; - while (current) { - if (current instanceof EmbeddedTemplateAst) { - for (const variable of current.variables) { - const name = variable.name; - - // Find the first directive with a context. - const context = - current.directives - .map(d => info.template.query.getTemplateContext(d.directive.type.reference)) - .find(c => !!c); - - // Determine the type of the context field referenced by variable.value. - let type: Symbol|undefined = undefined; - if (context) { - const value = context.get(variable.value); - if (value) { - type = value.type !; - let kind = info.template.query.getTypeKind(type); - if (kind === BuiltinType.Any || kind == BuiltinType.Unbound) { - // The any type is not very useful here. For special cases, such as ngFor, we can do - // better. - type = refinedVariableType(type, info, current); - } - } - } - if (!type) { - type = info.template.query.getBuiltinType(BuiltinType.Any); - } - result.push({ - name, - kind: 'variable', type, get definition() { return getDefintionOf(info, variable); } - }); - } - } - current = path.parentOf(current); - } - - return result; -} - -function refinedVariableType( - type: Symbol, info: TemplateInfo, templateElement: EmbeddedTemplateAst): Symbol { - // Special case the ngFor directive - const ngForDirective = templateElement.directives.find(d => { - const name = identifierName(d.directive.type); - return name == 'NgFor' || name == 'NgForOf'; - }); - if (ngForDirective) { - const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf'); - if (ngForOfBinding) { - const bindingType = - new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value); - if (bindingType) { - const result = info.template.query.getElementType(bindingType); - if (result) { - return result; - } - } - } - } - - // We can't do better, just return the original type. - return type; -} - -function getDefintionOf(info: TemplateInfo, ast: TemplateAst): Definition|undefined { - if (info.fileName) { - const templateOffset = info.template.span.start; - return [{ - fileName: info.fileName, - span: { - start: ast.sourceSpan.start.offset + templateOffset, - end: ast.sourceSpan.end.offset + templateOffset - } - }]; - } -} diff --git a/packages/language-service/src/html_path.ts b/packages/language-service/src/html_path.ts deleted file mode 100644 index 7cfd5ebf8e..0000000000 --- a/packages/language-service/src/html_path.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Attribute, Comment, Element, Expansion, ExpansionCase, Node, Text, Visitor, visitAll} from '@angular/compiler'; - -import {AstPath} from './ast_path'; -import {inSpan, spanOf} from './utils'; - -export class HtmlAstPath extends AstPath { - constructor(ast: Node[], public position: number) { super(buildPath(ast, position)); } -} - -function buildPath(ast: Node[], position: number): Node[] { - let visitor = new HtmlAstPathBuilder(position); - visitAll(visitor, ast); - return visitor.getPath(); -} - -export class ChildVisitor implements Visitor { - constructor(private visitor?: Visitor) {} - - visitElement(ast: Element, context: any): any { - this.visitChildren(context, visit => { - visit(ast.attrs); - visit(ast.children); - }); - } - - visitAttribute(ast: Attribute, context: any): any {} - visitText(ast: Text, context: any): any {} - visitComment(ast: Comment, context: any): any {} - - visitExpansion(ast: Expansion, context: any): any { - return this.visitChildren(context, visit => { visit(ast.cases); }); - } - - visitExpansionCase(ast: ExpansionCase, context: any): any {} - - private visitChildren( - context: any, cb: (visit: ((children: V[]|undefined) => void)) => void) { - const visitor = this.visitor || this; - let results: any[][] = []; - function visit(children: T[] | undefined) { - if (children) results.push(visitAll(visitor, children, context)); - } - cb(visit); - return [].concat.apply([], results); - } -} - -class HtmlAstPathBuilder extends ChildVisitor { - private path: Node[] = []; - - constructor(private position: number) { super(); } - - visit(ast: Node, context: any): any { - let span = spanOf(ast as any); - if (inSpan(this.position, span)) { - this.path.push(ast); - } else { - // Returning a value here will result in the children being skipped. - return true; - } - } - - getPath(): Node[] { return this.path; } -} diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index 65a481e6ad..e19f1dc28a 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileMetadataResolver, CompileNgModuleMetadata, CompilerConfig, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgAnalyzedModules, Parser, TemplateParser} from '@angular/compiler'; +import {CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompilerConfig, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgAnalyzedModules, Parser, TemplateParser} from '@angular/compiler'; import {AstResult, TemplateInfo} from './common'; import {getTemplateCompletions} from './completions'; @@ -48,12 +48,12 @@ class LanguageServiceImpl implements LanguageService { return uniqueBySpan(results); } - getPipesAt(fileName: string, position: number): Pipes { + getPipesAt(fileName: string, position: number): CompilePipeSummary[] { let templateInfo = this.getTemplateAstAtPosition(fileName, position); if (templateInfo) { - return templateInfo.pipes.map( - pipeInfo => ({name: pipeInfo.name, symbol: pipeInfo.type.reference})); + return templateInfo.pipes; } + return []; } getCompletionsAt(fileName: string, position: number): Completions { diff --git a/packages/language-service/src/locate_symbol.ts b/packages/language-service/src/locate_symbol.ts index 3cc93669bc..d9e497b5f1 100644 --- a/packages/language-service/src/locate_symbol.ts +++ b/packages/language-service/src/locate_symbol.ts @@ -6,14 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAst, tokenReference} from '@angular/compiler'; +import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAst, TemplateAstPath, findNode, tokenReference} from '@angular/compiler'; +import {getExpressionScope} from '@angular/compiler-cli'; import {TemplateInfo} from './common'; -import {getExpressionScope, getExpressionSymbol} from './expressions'; -import {HtmlAstPath} from './html_path'; -import {TemplateAstPath} from './template_path'; +import {getExpressionSymbol} from './expressions'; import {Definition, Location, Span, Symbol, SymbolTable} from './types'; -import {inSpan, offsetSpan, spanOf} from './utils'; +import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, inSpan, offsetSpan, spanOf} from './utils'; export interface SymbolInfo { symbol: Symbol; @@ -23,7 +22,7 @@ export interface SymbolInfo { export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { if (!info.position) return undefined; const templatePosition = info.position - info.template.span.start; - const path = new TemplateAstPath(info.templateAst, templatePosition); + const path = findTemplateAstAt(info.templateAst, templatePosition); if (path.tail) { let symbol: Symbol|undefined = undefined; let span: Span|undefined = undefined; @@ -31,7 +30,8 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { const attribute = findAttribute(info); if (attribute) { if (inSpan(templatePosition, spanOf(attribute.valueSpan))) { - const scope = getExpressionScope(info, path, inEvent); + const dinfo = diagnosticInfoFromTemplateInfo(info); + const scope = getExpressionScope(dinfo, path, inEvent); if (attribute.valueSpan) { const expressionOffset = attribute.valueSpan.start.offset + 1; const result = getExpressionSymbol( @@ -84,7 +84,8 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { visitBoundText(ast) { const expressionPosition = templatePosition - ast.sourceSpan.start.offset; if (inSpan(expressionPosition, ast.value.span)) { - const scope = getExpressionScope(info, path, /* includeEvent */ false); + const dinfo = diagnosticInfoFromTemplateInfo(info); + const scope = getExpressionScope(dinfo, path, /* includeEvent */ false); const result = getExpressionSymbol(scope, ast.value, expressionPosition, info.template.query); if (result) { @@ -115,7 +116,7 @@ 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 = new HtmlAstPath(info.htmlAst, templatePosition); + const path = findNode(info.htmlAst, templatePosition); return path.first(Attribute); } } @@ -184,6 +185,8 @@ class OverrideKindSymbol implements Symbol { get callable(): boolean { return this.sym.callable; } + get nullable(): boolean { return this.sym.nullable; } + get definition(): Definition { return this.sym.definition; } members() { return this.sym.members(); } diff --git a/packages/language-service/src/template_path.ts b/packages/language-service/src/template_path.ts deleted file mode 100644 index e974e39563..0000000000 --- a/packages/language-service/src/template_path.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler'; - -import {AstPath} from './ast_path'; -import {inSpan, isNarrower, spanOf} from './utils'; - -export class TemplateAstPath extends AstPath { - constructor(ast: TemplateAst[], public position: number, allowWidening: boolean = false) { - super(buildTemplatePath(ast, position, allowWidening)); - } -} - -function buildTemplatePath( - ast: TemplateAst[], position: number, allowWidening: boolean = false): TemplateAst[] { - const visitor = new TemplateAstPathBuilder(position, allowWidening); - templateVisitAll(visitor, ast); - return visitor.getPath(); -} - -export class NullTemplateVisitor implements TemplateAstVisitor { - visitNgContent(ast: NgContentAst): void {} - visitEmbeddedTemplate(ast: EmbeddedTemplateAst): void {} - visitElement(ast: ElementAst): void {} - visitReference(ast: ReferenceAst): void {} - visitVariable(ast: VariableAst): void {} - visitEvent(ast: BoundEventAst): void {} - visitElementProperty(ast: BoundElementPropertyAst): void {} - visitAttr(ast: AttrAst): void {} - visitBoundText(ast: BoundTextAst): void {} - visitText(ast: TextAst): void {} - visitDirective(ast: DirectiveAst): void {} - visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {} -} - -export class TemplateAstChildVisitor implements TemplateAstVisitor { - constructor(private visitor?: TemplateAstVisitor) {} - - // Nodes with children - visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { - return this.visitChildren(context, visit => { - visit(ast.attrs); - visit(ast.references); - visit(ast.variables); - visit(ast.directives); - visit(ast.providers); - visit(ast.children); - }); - } - - visitElement(ast: ElementAst, context: any): any { - return this.visitChildren(context, visit => { - visit(ast.attrs); - visit(ast.inputs); - visit(ast.outputs); - visit(ast.references); - visit(ast.directives); - visit(ast.providers); - visit(ast.children); - }); - } - - visitDirective(ast: DirectiveAst, context: any): any { - return this.visitChildren(context, visit => { - visit(ast.inputs); - visit(ast.hostProperties); - visit(ast.hostEvents); - }); - } - - // Terminal nodes - visitNgContent(ast: NgContentAst, context: any): any {} - visitReference(ast: ReferenceAst, context: any): any {} - visitVariable(ast: VariableAst, context: any): any {} - visitEvent(ast: BoundEventAst, context: any): any {} - visitElementProperty(ast: BoundElementPropertyAst, context: any): any {} - visitAttr(ast: AttrAst, context: any): any {} - visitBoundText(ast: BoundTextAst, context: any): any {} - visitText(ast: TextAst, context: any): any {} - visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {} - - protected visitChildren( - context: any, - cb: (visit: ((children: V[]|undefined) => void)) => void) { - const visitor = this.visitor || this; - let results: any[][] = []; - function visit(children: T[] | undefined) { - if (children && children.length) results.push(templateVisitAll(visitor, children, context)); - } - cb(visit); - return [].concat.apply([], results); - } -} - -class TemplateAstPathBuilder extends TemplateAstChildVisitor { - private path: TemplateAst[] = []; - - constructor(private position: number, private allowWidening: boolean) { super(); } - - visit(ast: TemplateAst, context: any): any { - let span = spanOf(ast); - if (inSpan(this.position, span)) { - const len = this.path.length; - if (!len || this.allowWidening || isNarrower(span, spanOf(this.path[len - 1]))) { - this.path.push(ast); - } - } else { - // Returning a value here will result in the children being skipped. - return true; - } - } - - visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { - return this.visitChildren(context, visit => { - // Ignore reference, variable and providers - visit(ast.attrs); - visit(ast.directives); - visit(ast.children); - }); - } - - visitElement(ast: ElementAst, context: any): any { - return this.visitChildren(context, visit => { - // Ingnore providers - visit(ast.attrs); - visit(ast.inputs); - visit(ast.outputs); - visit(ast.references); - visit(ast.directives); - visit(ast.children); - }); - } - - visitDirective(ast: DirectiveAst, context: any): any { - // Ignore the host properties of a directive - const result = this.visitChildren(context, visit => { visit(ast.inputs); }); - // We never care about the diretive itself, just its inputs. - if (this.path[this.path.length - 1] == ast) { - this.path.pop(); - } - return result; - } - - getPath() { return this.path; } -} diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index 9e8c46403a..b20b0a4110 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -6,31 +6,26 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompileMetadataResolver, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; +import {CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; +import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from '@angular/compiler-cli'; -/** - * The range of a span of text in a source file. - * - * @experimental - */ -export interface Span { - /** - * The first code-point of the span as an offset relative to the beginning of the source assuming - * a UTF-16 encoding. - */ - start: number; - - /** - * The first code-point after the span as an offset relative to the beginning of the source - * assuming a UTF-16 encoding. - */ - end: number; -} +export { + BuiltinType, + DeclarationKind, + Definition, + PipeInfo, + Pipes, + Signature, + Span, + Symbol, + SymbolDeclaration, + SymbolQuery, + SymbolTable +}; /** * The information `LanguageService` needs from the `LanguageServiceHost` to describe the content of - * a template and the - * langauge context the template is in. + * a template and the langauge context the template is in. * * A host interface; see `LanguageSeriviceHost`. * @@ -44,11 +39,9 @@ export interface TemplateSource { /** * The version of the source. As files are modified the version should change. That is, if the - * `LanguageService` requesting - * template infomration for a source file and that file has changed since the last time the host - * was asked for the file then - * this version string should be different. No assumptions are made about the format of this - * string. + * `LanguageService` requesting template infomration for a source file and that file has changed + * since the last time the host was asked for the file then this version string should be + * different. No assumptions are made about the format of this string. * * The version can change more often than the source but should not change less often. */ @@ -84,6 +77,7 @@ export interface TemplateSource { */ export type TemplateSources = TemplateSource[] | undefined; + /** * Error information found getting declaration information * @@ -149,266 +143,8 @@ export interface Declaration { export type Declarations = Declaration[]; /** - * An enumeration of basic types. - * - * A `LanguageServiceHost` interface. - * - * @experimental - */ -export enum BuiltinType { - /** - * The type is a type that can hold any other type. - */ - Any, - - /** - * The type of a string literal. - */ - String, - - /** - * The type of a numeric literal. - */ - Number, - - /** - * The type of the `true` and `false` literals. - */ - Boolean, - - /** - * The type of the `undefined` literal. - */ - Undefined, - - /** - * the type of the `null` literal. - */ - Null, - - /** - * the type is an unbound type parameter. - */ - Unbound, - - /** - * Not a built-in type. - */ - Other -} - -/** - * A symbol describing a language element that can be referenced by expressions - * in an Angular template. - * - * A `LanguageServiceHost` interface. - * - * @experimental - */ -export interface Symbol { - /** - * The name of the symbol as it would be referenced in an Angular expression. - */ - readonly name: string; - - /** - * The kind of completion this symbol should generate if included. - */ - readonly kind: string; - - /** - * The language of the source that defines the symbol. (e.g. typescript for TypeScript, - * ng-template for an Angular template, etc.) - */ - readonly language: string; - - /** - * A symbol representing type of the symbol. - */ - readonly type: Symbol|undefined; - - - /** - * A symbol for the container of this symbol. For example, if this is a method, the container - * is the class or interface of the method. If no container is appropriate, undefined is - * returned. - */ - readonly container: Symbol|undefined; - - /** - * The symbol is public in the container. - */ - readonly public: boolean; - - /** - * `true` if the symbol can be the target of a call. - */ - readonly callable: boolean; - - /** - * The location of the definition of the symbol - */ - readonly definition: Definition|undefined; - /** - - * A table of the members of the symbol; that is, the members that can appear - * after a `.` in an Angular expression. - * - */ - members(): SymbolTable; - - /** - * The list of overloaded signatures that can be used if the symbol is the - * target of a call. - */ - signatures(): Signature[]; - - /** - * Return which signature of returned by `signatures()` would be used selected - * given the `types` supplied. If no signature would match, this method should - * return `undefined`. - */ - selectSignature(types: Symbol[]): Signature|undefined; - - /** - * Return the type of the expression if this symbol is indexed by `argument`. - * If the symbol cannot be indexed, this method should return `undefined`. - */ - indexed(argument: Symbol): Symbol|undefined; -} - -/** - * A table of `Symbol`s accessible by name. - * - * A `LanguageServiceHost` interface. - * - * @experimental - */ -export interface SymbolTable { - /** - * The number of symbols in the table. - */ - readonly size: number; - - /** - * Get the symbol corresponding to `key` or `undefined` if there is no symbol in the - * table by the name `key`. - */ - get(key: string): Symbol|undefined; - - /** - * Returns `true` if the table contains a `Symbol` with the name `key`. - */ - has(key: string): boolean; - - /** - * Returns all the `Symbol`s in the table. The order should be, but is not required to be, - * in declaration order. - */ - values(): Symbol[]; -} - -/** - * A description of a function or method signature. - * - * A `LanguageServiceHost` interface. - * - * @experimental - */ -export interface Signature { - /** - * The arguments of the signture. The order of `argumetnts.symbols()` must be in the order - * of argument declaration. - */ - readonly arguments: SymbolTable; - - /** - * The symbol of the signature result type. - */ - readonly result: Symbol; -} - -/** - * Describes the language context in which an Angular expression is evaluated. - * - * A `LanguageServiceHost` interface. - * - * @experimental - */ -export interface SymbolQuery { - /** - * Return the built-in type this symbol represents or Other if it is not a built-in type. - */ - getTypeKind(symbol: Symbol): BuiltinType; - - /** - * Return a symbol representing the given built-in type. - */ - getBuiltinType(kind: BuiltinType): Symbol; - - /** - * Return the symbol for a type that represents the union of all the types given. Any value - * of one of the types given should be assignable to the returned type. If no one type can - * be constructed then this should be the Any type. - */ - getTypeUnion(...types: Symbol[]): Symbol; - - /** - * Return a symbol for an array type that has the `type` as its element type. - */ - getArrayType(type: Symbol): Symbol; - - /** - * Return element type symbol for an array type if the `type` is an array type. Otherwise return - * undefined. - */ - getElementType(type: Symbol): Symbol|undefined; - - /** - * Return a type that is the non-nullable version of the given type. If `type` is already - * non-nullable, return `type`. - */ - getNonNullableType(type: Symbol): Symbol; - - /** - * Return a symbol table for the pipes that are in scope. - */ - getPipes(): SymbolTable; - - /** - * Return the type symbol for the given static symbol. - */ - getTypeSymbol(type: StaticSymbol): Symbol; - - /** - * Return the members that are in the context of a type's template reference. - */ - getTemplateContext(type: StaticSymbol): SymbolTable|undefined; - - /** - * Produce a symbol table with the given symbols. Used to produce a symbol table - * for use with mergeSymbolTables(). - */ - createSymbolTable(symbols: SymbolDeclaration[]): SymbolTable; - - /** - * Produce a merged symbol table. If the symbol tables contain duplicate entries - * the entries of the latter symbol tables will obscure the entries in the prior - * symbol tables. - * - * The symbol tables passed to this routine MUST be produces by the same instance - * of SymbolQuery that is being called. - */ - mergeSymbolTable(symbolTables: SymbolTable[]): SymbolTable; - - /** - * Return the span of the narrowest non-token node at the given location. - */ - getSpanAt(line: number, column: number): Span|undefined; -} - -/** - * The host for a `LanguageService`. This provides all the `LanguageService` requires to respond to + * The host for a `LanguageService`. This provides all the `LanguageService` requires to respond + * to * the `LanguageService` requests. * * This interface describes the requirements of the `LanguageService` on its host. @@ -416,28 +152,21 @@ export interface SymbolQuery { * The host interface is host language agnostic. * * Adding optional member to this interface or any interface that is described as a - * `LanguageServiceHost` - * interface is not considered a breaking change as defined by SemVer. Removing a method or changing - * a - * member from required to optional will also not be considered a breaking change. + * `LanguageServiceHost` interface is not considered a breaking change as defined by SemVer. + * Removing a method or changing a member from required to optional will also not be considered a + * breaking change. * - * If a member is deprecated it will be changed to optional in a minor release before it is removed - * in - * a major release. + * If a member is deprecated it will be changed to optional in a minor release before it is + * removed in a major release. * * Adding a required member or changing a method's parameters, is considered a breaking change and - * will - * only be done when breaking changes are allowed. When possible, a new optional member will be - * added and - * the old member will be deprecated. The new member will then be made required in and the old - * member will - * be removed only when breaking chnages are allowed. + * will only be done when breaking changes are allowed. When possible, a new optional member will + * be added and the old member will be deprecated. The new member will then be made required in + * and the old member will be removed only when breaking chnages are allowed. * * While an interface is marked as experimental breaking-changes will be allowed between minor - * releases. - * After an interface is marked as stable breaking-changes will only be allowed between major - * releases. - * No breaking changes are allowed between patch releases. + * releases. After an interface is marked as stable breaking-changes will only be allowed between + * major releases. No breaking changes are allowed between patch releases. * * @experimental */ @@ -449,16 +178,15 @@ export interface LanguageServiceHost { /** * Returns the template information for templates in `fileName` at the given location. If - * `fileName` - * 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`. + * `fileName` 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`. */ getTemplateAt(fileName: string, position: number): TemplateSource|undefined; /** - * Return the template source information for all templates in `fileName` or for `fileName` if it - * is - * a template file. + * Return the template source information for all templates in `fileName` or for `fileName` if + * it + * is a template file. */ getTemplates(fileName: string): TemplateSources; @@ -478,16 +206,6 @@ export interface LanguageServiceHost { getTemplateReferences(): string[]; } -/** - * The kinds of completions generated by the language service. - * - * A 'LanguageService' interface. - * - * @experimental - */ -export type CompletionKind = 'attribute' | 'html attribute' | 'component' | 'element' | 'entity' | - 'key' | 'method' | 'pipe' | 'property' | 'type' | 'reference' | 'variable'; - /** * An item of the completion result to be displayed by an editor. * @@ -499,7 +217,7 @@ export interface Completion { /** * The kind of comletion. */ - kind: CompletionKind; + kind: DeclarationKind; /** * The name of the completion to be displayed @@ -527,11 +245,6 @@ export interface Location { span: Span; } -/** - * A defnition location(s). - */ -export type Definition = Location[] | undefined; - /** * The kind of diagnostic message. * @@ -571,62 +284,6 @@ export interface Diagnostic { */ export type Diagnostics = Diagnostic[]; -/** - * Information about the pipes that are available for use in a template. - * - * A `LanguageService` interface. - * - * @experimental - */ -export interface PipeInfo { - /** - * The name of the pipe. - */ - name: string; - - /** - * The static symbol for the pipe's constructor. - */ - symbol: StaticSymbol; -} - -/** - * A sequence of pipe information. - * - * @experimental - */ -export type Pipes = PipeInfo[] | undefined; - -/** - * Describes a symbol to type binding used to build a symbol table. - * - * A `LanguageServiceHost` interface. - * - * @experimental - */ - -export interface SymbolDeclaration { - /** - * The name of the symbol in table. - */ - readonly name: string; - - /** - * The kind of symbol to declare. - */ - readonly kind: CompletionKind; - - /** - * Type of the symbol. The type symbol should refer to a symbol for a type. - */ - readonly type: Symbol; - - /** - * The definion of the symbol if one exists. - */ - readonly definition?: Definition; -} - /** * A section of hover text. If the text is code then langauge should be provided. * Otherwise the text is assumed to be Markdown text that will be sanitized. @@ -663,33 +320,26 @@ export interface Hover { * An instance of an Angular language service created by `createLanguageService()`. * * The language service returns information about Angular templates that are included in a project - * as - * defined by the `LanguageServiceHost`. + * as defined by the `LanguageServiceHost`. * * When a method expects a `fileName` this file can either be source file in the project that - * contains - * a template in a string literal or a template file referenced by the project returned by - * `getTemplateReference()`. All other files will cause the method to return `undefined`. + * contains a template in a string literal or a template file referenced by the project returned + * by `getTemplateReference()`. All other files will cause the method to return `undefined`. * * If a method takes a `position`, it is the offset of the UTF-16 code-point relative to the - * beginning - * of the file reference by `fileName`. + * beginning of the file reference by `fileName`. * * This interface and all interfaces and types marked as `LanguageService` types, describe a - * particlar - * implementation of the Angular language service and is not intented to be implemented. Adding - * members - * to the interface will not be considered a breaking change as defined by SemVer. + * particlar implementation of the Angular language service and is not intented to be + * implemented. Adding members to the interface will not be considered a breaking change as + * defined by SemVer. * * Removing a member or making a member optional, changing a method parameters, or changing a - * member's - * type will all be considered a breaking change. + * member's type will all be considered a breaking change. * * While an interface is marked as experimental breaking-changes will be allowed between minor - * releases. - * After an interface is marked as stable breaking-changes will only be allowed between major - * releases. - * No breaking changes are allowed between patch releases. + * releases. After an interface is marked as stable breaking-changes will only be allowed between + * major releases. No breaking changes are allowed between patch releases. * * @experimental */ @@ -722,5 +372,5 @@ export interface LanguageService { /** * Return the pipes that are available at the given position. */ - getPipesAt(fileName: string, position: number): Pipes|undefined; + getPipesAt(fileName: string, position: number): CompilePipeSummary[]; } diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index 3278288dbf..dc9e54dfc5 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticAndDynamicReflectionCapabilities, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, analyzeNgModules, componentModuleUrl, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; -import {AngularCompilerOptions} from '@angular/compiler-cli'; +import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticAndDynamicReflectionCapabilities, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, componentModuleUrl, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; +import {AngularCompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as fs from 'fs'; import * as path from 'path'; @@ -15,24 +15,10 @@ import * as ts from 'typescript'; import {createLanguageService} from './language_service'; import {ReflectorHost} from './reflector_host'; -import {BuiltinType, CompletionKind, Declaration, DeclarationError, Declarations, Definition, LanguageService, LanguageServiceHost, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types'; +import {BuiltinType, Declaration, DeclarationError, DeclarationKind, Declarations, Definition, LanguageService, LanguageServiceHost, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types'; import {isTypescriptVersion} from './utils'; - -// In TypeScript 2.1 these flags moved -// These helpers work for both 2.0 and 2.1. -const isPrivate = (ts as any).ModifierFlags ? - ((node: ts.Node) => - !!((ts as any).getCombinedModifierFlags(node) & (ts as any).ModifierFlags.Private)) : - ((node: ts.Node) => !!(node.flags & (ts as any).NodeFlags.Private)); - -const isReferenceType = (ts as any).ObjectFlags ? - ((type: ts.Type) => - !!(type.flags & (ts as any).TypeFlags.Object && - (type as any).objectFlags & (ts as any).ObjectFlags.Reference)) : - ((type: ts.Type) => !!(type.flags & (ts as any).TypeFlags.Reference)); - /** * Create a `LanguageServiceHost` */ @@ -316,19 +302,17 @@ export class TypeScriptServiceHost implements LanguageServiceHost { source, span, type, - get members(): - SymbolTable{const checker = t.checker; const program = t.program; - const type = checker.getTypeAtLocation(declaration); - return new TypeWrapper(type, {node, program, checker}).members();}, - get query(): SymbolQuery{ + get members() { + return getClassMembersFromDeclaration(t.program, t.checker, sourceFile, declaration); + }, + get query() { if (!queryCache) { - queryCache = new TypeScriptSymbolQuery(t.program, t.checker, sourceFile, () => { - const pipes = t.service.getPipesAt(fileName, node.getStart()); - const checker = t.checker; - const program = t.program; - return new PipesTable(pipes, {node, program, checker}); - }); - } return queryCache; + const pipes = t.service.getPipesAt(fileName, node.getStart()); + queryCache = getSymbolQuery( + t.program, t.checker, sourceFile, + () => getPipesTable(sourceFile, t.program, t.checker, pipes)); + } + return queryCache; } }; } @@ -575,563 +559,8 @@ export class TypeScriptServiceHost implements LanguageServiceHost { return find(sourceFile); } - - private findLiteralType(kind: BuiltinType, context: TypeContext): Symbol { - const checker = this.checker; - let type: ts.Type; - switch (kind) { - case BuiltinType.Any: - type = checker.getTypeAtLocation({ - kind: ts.SyntaxKind.AsExpression, - expression: {kind: ts.SyntaxKind.TrueKeyword}, - type: {kind: ts.SyntaxKind.AnyKeyword} - }); - break; - case BuiltinType.Boolean: - type = checker.getTypeAtLocation({kind: ts.SyntaxKind.TrueKeyword}); - break; - case BuiltinType.Null: - type = checker.getTypeAtLocation({kind: ts.SyntaxKind.NullKeyword}); - break; - case BuiltinType.Number: - type = checker.getTypeAtLocation({kind: ts.SyntaxKind.NumericLiteral}); - break; - case BuiltinType.String: - type = - checker.getTypeAtLocation({kind: ts.SyntaxKind.NoSubstitutionTemplateLiteral}); - break; - case BuiltinType.Undefined: - type = checker.getTypeAtLocation({kind: ts.SyntaxKind.VoidExpression}); - break; - default: - throw new Error(`Internal error, unhandled literal kind ${kind}:${BuiltinType[kind]}`); - } - return new TypeWrapper(type, context); - } } -class TypeScriptSymbolQuery implements SymbolQuery { - private typeCache = new Map(); - private pipesCache: SymbolTable; - - constructor( - private program: ts.Program, private checker: ts.TypeChecker, private source: ts.SourceFile, - private fetchPipes: () => SymbolTable) {} - - getTypeKind(symbol: Symbol): BuiltinType { return typeKindOf(this.getTsTypeOf(symbol)); } - - getBuiltinType(kind: BuiltinType): Symbol { - // TODO: Replace with typeChecker API when available. - let result = this.typeCache.get(kind); - if (!result) { - const type = getBuiltinTypeFromTs( - kind, {checker: this.checker, node: this.source, program: this.program}); - result = - new TypeWrapper(type, {program: this.program, checker: this.checker, node: this.source}); - this.typeCache.set(kind, result); - } - return result; - } - - getTypeUnion(...types: Symbol[]): Symbol { - // TODO: Replace with typeChecker API when available - // No API exists so the cheat is to just return the last type any if no types are given. - return types.length ? types[types.length - 1] : this.getBuiltinType(BuiltinType.Any); - } - - getArrayType(type: Symbol): Symbol { - // TODO: Replace with typeChecker API when available - return this.getBuiltinType(BuiltinType.Any); - } - - getElementType(type: Symbol): Symbol|undefined { - if (type instanceof TypeWrapper) { - const elementType = getTypeParameterOf(type.tsType, 'Array'); - if (elementType) { - return new TypeWrapper(elementType, type.context); - } - } - } - - getNonNullableType(symbol: Symbol): Symbol { - if (symbol instanceof TypeWrapper && (typeof this.checker.getNonNullableType == 'function')) { - const tsType = symbol.tsType; - const nonNullableType = this.checker.getNonNullableType(tsType); - if (nonNullableType != tsType) { - return new TypeWrapper(nonNullableType, symbol.context); - } - } - return this.getBuiltinType(BuiltinType.Any); - } - - getPipes(): SymbolTable { - let result = this.pipesCache; - if (!result) { - result = this.pipesCache = this.fetchPipes(); - } - return result; - } - - getTemplateContext(type: StaticSymbol): SymbolTable|undefined { - const context: TypeContext = {node: this.source, program: this.program, checker: this.checker}; - const typeSymbol = findClassSymbolInContext(type, context); - if (typeSymbol) { - const contextType = this.getTemplateRefContextType(typeSymbol); - if (contextType) return new SymbolWrapper(contextType, context).members(); - } - } - - getTypeSymbol(type: StaticSymbol): Symbol { - const context: TypeContext = {node: this.source, program: this.program, checker: this.checker}; - const typeSymbol = findClassSymbolInContext(type, context) !; - return new SymbolWrapper(typeSymbol, context); - } - - createSymbolTable(symbols: SymbolDeclaration[]): SymbolTable { - const result = new MapSymbolTable(); - result.addAll(symbols.map(s => new DeclaredSymbol(s))); - return result; - } - - mergeSymbolTable(symbolTables: SymbolTable[]): SymbolTable { - const result = new MapSymbolTable(); - for (const symbolTable of symbolTables) { - result.addAll(symbolTable.values()); - } - return result; - } - - getSpanAt(line: number, column: number): Span|undefined { - return spanAt(this.source, line, column); - } - - private getTemplateRefContextType(typeSymbol: ts.Symbol): ts.Symbol|undefined { - const type = this.checker.getTypeOfSymbolAtLocation(typeSymbol, this.source); - const constructor = type.symbol && type.symbol.members && - getFromSymbolTable(type.symbol.members !, '__constructor'); - - if (constructor) { - const constructorDeclaration = constructor.declarations ![0] as ts.ConstructorTypeNode; - for (const parameter of constructorDeclaration.parameters) { - const type = this.checker.getTypeAtLocation(parameter.type !); - if (type.symbol !.name == 'TemplateRef' && isReferenceType(type)) { - const typeReference = type as ts.TypeReference; - if (typeReference.typeArguments.length === 1) { - return typeReference.typeArguments[0].symbol; - } - } - } - } - } - - private getTsTypeOf(symbol: Symbol): ts.Type|undefined { - const type = this.getTypeWrapper(symbol); - return type && type.tsType; - } - - private getTypeWrapper(symbol: Symbol): TypeWrapper|undefined { - let type: TypeWrapper|undefined = undefined; - if (symbol instanceof TypeWrapper) { - type = symbol; - } else if (symbol.type instanceof TypeWrapper) { - type = symbol.type; - } - return type; - } -} - -interface TypeContext { - node: ts.Node; - program: ts.Program; - checker: ts.TypeChecker; -} - -function typeCallable(type: ts.Type): boolean { - const signatures = type.getCallSignatures(); - return signatures && signatures.length != 0; -} - -function signaturesOf(type: ts.Type, context: TypeContext): Signature[] { - return type.getCallSignatures().map(s => new SignatureWrapper(s, context)); -} - -function selectSignature(type: ts.Type, context: TypeContext, types: Symbol[]): Signature| - undefined { - // TODO: Do a better job of selecting the right signature. - const signatures = type.getCallSignatures(); - return signatures.length ? new SignatureWrapper(signatures[0], context) : undefined; -} - -class TypeWrapper implements Symbol { - constructor(public tsType: ts.Type, public context: TypeContext) { - if (!tsType) { - throw Error('Internal: null type'); - } - } - - get name(): string { - const symbol = this.tsType.symbol; - return (symbol && symbol.name) || ''; - } - - get kind(): CompletionKind { return 'type'; } - - get language(): string { return 'typescript'; } - - get type(): Symbol|undefined { return undefined; } - - get container(): Symbol|undefined { return undefined; } - - get public(): boolean { return true; } - - get callable(): boolean { return typeCallable(this.tsType); } - - get definition(): Definition { return definitionFromTsSymbol(this.tsType.getSymbol()); } - - members(): SymbolTable { - return new SymbolTableWrapper(this.tsType.getProperties(), this.context); - } - - signatures(): Signature[] { return signaturesOf(this.tsType, this.context); } - - selectSignature(types: Symbol[]): Signature|undefined { - return selectSignature(this.tsType, this.context, types); - } - - indexed(argument: Symbol): Symbol|undefined { return undefined; } -} - -class SymbolWrapper implements Symbol { - private symbol: ts.Symbol; - private _tsType: ts.Type; - private _members: SymbolTable; - - constructor(symbol: ts.Symbol, private context: TypeContext) { - this.symbol = symbol && context && (symbol.flags & ts.SymbolFlags.Alias) ? - context.checker.getAliasedSymbol(symbol) : - symbol; - } - - get name(): string { return this.symbol.name; } - - get kind(): CompletionKind { return this.callable ? 'method' : 'property'; } - - get language(): string { return 'typescript'; } - - get type(): Symbol|undefined { return new TypeWrapper(this.tsType, this.context); } - - get container(): Symbol|undefined { return getContainerOf(this.symbol, this.context); } - - get public(): boolean { - // Symbols that are not explicitly made private are public. - return !isSymbolPrivate(this.symbol); - } - - get callable(): boolean { return typeCallable(this.tsType); } - - get definition(): Definition { return definitionFromTsSymbol(this.symbol); } - - members(): SymbolTable { - if (!this._members) { - if ((this.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) != 0) { - const declaredType = this.context.checker.getDeclaredTypeOfSymbol(this.symbol); - const typeWrapper = new TypeWrapper(declaredType, this.context); - this._members = typeWrapper.members(); - } else { - this._members = new SymbolTableWrapper(this.symbol.members !, this.context); - } - } - return this._members; - } - - signatures(): Signature[] { return signaturesOf(this.tsType, this.context); } - - selectSignature(types: Symbol[]): Signature|undefined { - return selectSignature(this.tsType, this.context, types); - } - - indexed(argument: Symbol): Symbol|undefined { return undefined; } - - private get tsType(): ts.Type { - let type = this._tsType; - if (!type) { - type = this._tsType = - this.context.checker.getTypeOfSymbolAtLocation(this.symbol, this.context.node); - } - return type; - } -} - -class DeclaredSymbol implements Symbol { - constructor(private declaration: SymbolDeclaration) {} - - get name() { return this.declaration.name; } - - get kind() { return this.declaration.kind; } - - get language(): string { return 'ng-template'; } - - get container(): Symbol|undefined { return undefined; } - - get type() { return this.declaration.type; } - - get callable(): boolean { return this.declaration.type.callable; } - - get public(): boolean { return true; } - - get definition(): Definition { return this.declaration.definition; } - - members(): SymbolTable { return this.declaration.type.members(); } - - signatures(): Signature[] { return this.declaration.type.signatures(); } - - selectSignature(types: Symbol[]): Signature|undefined { - return this.declaration.type.selectSignature(types); - } - - indexed(argument: Symbol): Symbol|undefined { return undefined; } -} - -class SignatureWrapper implements Signature { - constructor(private signature: ts.Signature, private context: TypeContext) {} - - get arguments(): SymbolTable { - return new SymbolTableWrapper(this.signature.getParameters(), this.context); - } - - get result(): Symbol { return new TypeWrapper(this.signature.getReturnType(), this.context); } -} - -class SignatureResultOverride implements Signature { - constructor(private signature: Signature, private resultType: Symbol) {} - - get arguments(): SymbolTable { return this.signature.arguments; } - - get result(): Symbol { return this.resultType; } -} - -function toSymbolTable(symbols: ts.Symbol[]): ts.SymbolTable { - if (isTypescriptVersion('2.2')) { - const result = new Map(); - for (const symbol of symbols) { - result.set(symbol.name, symbol); - } - return (result as any); - } - - const result = {}; - for (const symbol of symbols) { - result[symbol.name] = symbol; - } - return result as ts.SymbolTable; -} - -function toSymbols(symbolTable: ts.SymbolTable | undefined): ts.Symbol[] { - if (!symbolTable) return []; - - const table = symbolTable as any; - - if (typeof table.values === 'function') { - return Array.from(table.values()) as ts.Symbol[]; - } - - const result: ts.Symbol[] = []; - - const own = typeof table.hasOwnProperty === 'function' ? - (name: string) => table.hasOwnProperty(name) : - (name: string) => !!table[name]; - - for (const name in table) { - if (own(name)) { - result.push(table[name]); - } - } - return result; -} - -class SymbolTableWrapper implements SymbolTable { - private symbols: ts.Symbol[]; - private symbolTable: ts.SymbolTable; - - constructor(symbols: ts.SymbolTable|ts.Symbol[]|undefined, private context: TypeContext) { - symbols = symbols || []; - - if (Array.isArray(symbols)) { - this.symbols = symbols; - this.symbolTable = toSymbolTable(symbols); - } else { - this.symbols = toSymbols(symbols); - this.symbolTable = symbols; - } - } - - get size(): number { return this.symbols.length; } - - get(key: string): Symbol|undefined { - const symbol = getFromSymbolTable(this.symbolTable, key); - return symbol ? new SymbolWrapper(symbol, this.context) : undefined; - } - - has(key: string): boolean { - const table: any = this.symbolTable; - return (typeof table.has === 'function') ? table.has(key) : table[key] != null; - } - - values(): Symbol[] { return this.symbols.map(s => new SymbolWrapper(s, this.context)); } -} - -class MapSymbolTable implements SymbolTable { - private map = new Map(); - private _values: Symbol[] = []; - - get size(): number { return this.map.size; } - - get(key: string): Symbol|undefined { return this.map.get(key); } - - add(symbol: Symbol) { - if (this.map.has(symbol.name)) { - const previous = this.map.get(symbol.name) !; - this._values[this._values.indexOf(previous)] = symbol; - } - this.map.set(symbol.name, symbol); - this._values.push(symbol); - } - - addAll(symbols: Symbol[]) { - for (const symbol of symbols) { - this.add(symbol); - } - } - - has(key: string): boolean { return this.map.has(key); } - - values(): Symbol[] { - // Switch to this.map.values once iterables are supported by the target language. - return this._values; - } -} - -class PipesTable implements SymbolTable { - constructor(private pipes: Pipes, private context: TypeContext) {} - - get size() { return this.pipes !.length; } - - get(key: string): Symbol|undefined { - const pipe = this.pipes !.find(pipe => pipe.name == key); - if (pipe) { - return new PipeSymbol(pipe, this.context); - } - } - - has(key: string): boolean { return this.pipes !.find(pipe => pipe.name == key) != null; } - - values(): Symbol[] { return this.pipes !.map(pipe => new PipeSymbol(pipe, this.context)); } -} - -class PipeSymbol implements Symbol { - private _tsType: ts.Type; - - constructor(private pipe: PipeInfo, private context: TypeContext) {} - - get name(): string { return this.pipe.name; } - - get kind(): CompletionKind { return 'pipe'; } - - get language(): string { return 'typescript'; } - - get type(): Symbol|undefined { return new TypeWrapper(this.tsType, this.context); } - - get container(): Symbol|undefined { return undefined; } - - get callable(): boolean { return true; } - - get public(): boolean { return true; } - - get definition(): Definition { return definitionFromTsSymbol(this.tsType.getSymbol()); } - - members(): SymbolTable { return EmptyTable.instance; } - - signatures(): Signature[] { return signaturesOf(this.tsType, this.context); } - - selectSignature(types: Symbol[]): Signature|undefined { - let signature = selectSignature(this.tsType, this.context, types) !; - if (types.length == 1) { - const parameterType = types[0]; - if (parameterType instanceof TypeWrapper) { - let resultType: ts.Type|undefined = undefined; - switch (this.name) { - case 'async': - switch (parameterType.name) { - case 'Observable': - case 'Promise': - case 'EventEmitter': - resultType = getTypeParameterOf(parameterType.tsType, parameterType.name); - break; - default: - resultType = getBuiltinTypeFromTs(BuiltinType.Any, this.context); - break; - } - break; - case 'slice': - resultType = getTypeParameterOf(parameterType.tsType, 'Array'); - break; - } - if (resultType) { - signature = new SignatureResultOverride( - signature, new TypeWrapper(resultType, parameterType.context)); - } - } - } - return signature; - } - - indexed(argument: Symbol): Symbol|undefined { return undefined; } - - private get tsType(): ts.Type { - let type = this._tsType; - if (!type) { - const classSymbol = this.findClassSymbol(this.pipe.symbol); - if (classSymbol) { - type = this._tsType = this.findTransformMethodType(classSymbol) !; - } - if (!type) { - type = this._tsType = getBuiltinTypeFromTs(BuiltinType.Any, this.context); - } - } - return type; - } - - private findClassSymbol(type: StaticSymbol): ts.Symbol|undefined { - return findClassSymbolInContext(type, this.context); - } - - private findTransformMethodType(classSymbol: ts.Symbol): ts.Type|undefined { - const classType = this.context.checker.getDeclaredTypeOfSymbol(classSymbol); - if (classType) { - const transform = classType.getProperty('transform'); - if (transform) { - return this.context.checker.getTypeOfSymbolAtLocation(transform, this.context.node); - } - } - } -} - -function findClassSymbolInContext(type: StaticSymbol, context: TypeContext): ts.Symbol|undefined { - const sourceFile = context.program.getSourceFile(type.filePath); - if (sourceFile) { - const moduleSymbol = (sourceFile as any).module || (sourceFile as any).symbol; - const exports = context.checker.getExportsOfModule(moduleSymbol); - return (exports || []).find(symbol => symbol.name == type.name); - } -} - -class EmptyTable implements SymbolTable { - get size(): number { return 0; } - get(key: string): Symbol|undefined { return undefined; } - has(key: string): boolean { return false; } - values(): Symbol[] { return []; } - static instance = new EmptyTable(); -} function findTsConfig(fileName: string): string|undefined { let dir = path.dirname(fileName); @@ -1144,94 +573,6 @@ function findTsConfig(fileName: string): string|undefined { } } -function isBindingPattern(node: ts.Node): node is ts.BindingPattern { - return !!node && (node.kind === ts.SyntaxKind.ArrayBindingPattern || - node.kind === ts.SyntaxKind.ObjectBindingPattern); -} - -function walkUpBindingElementsAndPatterns(node: ts.Node): ts.Node { - while (node && (node.kind === ts.SyntaxKind.BindingElement || isBindingPattern(node))) { - node = node.parent !; - } - - return node; -} - -function getCombinedNodeFlags(node: ts.Node): ts.NodeFlags { - node = walkUpBindingElementsAndPatterns(node); - - let flags = node.flags; - if (node.kind === ts.SyntaxKind.VariableDeclaration) { - node = node.parent !; - } - - if (node && node.kind === ts.SyntaxKind.VariableDeclarationList) { - flags |= node.flags; - node = node.parent !; - } - - if (node && node.kind === ts.SyntaxKind.VariableStatement) { - flags |= node.flags; - } - - return flags; -} - -function isSymbolPrivate(s: ts.Symbol): boolean { - return !!s.valueDeclaration && isPrivate(s.valueDeclaration); -} - -function getBuiltinTypeFromTs(kind: BuiltinType, context: TypeContext): ts.Type { - let type: ts.Type; - const checker = context.checker; - const node = context.node; - switch (kind) { - case BuiltinType.Any: - type = checker.getTypeAtLocation(setParents( - { - kind: ts.SyntaxKind.AsExpression, - expression: {kind: ts.SyntaxKind.TrueKeyword}, - type: {kind: ts.SyntaxKind.AnyKeyword} - }, - node)); - break; - case BuiltinType.Boolean: - type = - checker.getTypeAtLocation(setParents({kind: ts.SyntaxKind.TrueKeyword}, node)); - break; - case BuiltinType.Null: - type = - checker.getTypeAtLocation(setParents({kind: ts.SyntaxKind.NullKeyword}, node)); - break; - case BuiltinType.Number: - const numeric = {kind: ts.SyntaxKind.NumericLiteral}; - setParents({kind: ts.SyntaxKind.ExpressionStatement, expression: numeric}, node); - type = checker.getTypeAtLocation(numeric); - break; - case BuiltinType.String: - type = checker.getTypeAtLocation( - setParents({kind: ts.SyntaxKind.NoSubstitutionTemplateLiteral}, node)); - break; - case BuiltinType.Undefined: - type = checker.getTypeAtLocation(setParents( - { - kind: ts.SyntaxKind.VoidExpression, - expression: {kind: ts.SyntaxKind.NumericLiteral} - }, - node)); - break; - default: - throw new Error(`Internal error, unhandled literal kind ${kind}:${BuiltinType[kind]}`); - } - return type; -} - -function setParents(node: T, parent: ts.Node): T { - node.parent = parent; - ts.forEachChild(node, child => setParents(child, node)); - return node; -} - function spanOf(node: ts.Node): Span { return {start: node.getStart(), end: node.getEnd()}; } @@ -1257,103 +598,3 @@ function spanAt(sourceFile: ts.SourceFile, line: number, column: number): Span|u } } } - -function definitionFromTsSymbol(symbol: ts.Symbol): Definition { - const declarations = symbol.declarations; - if (declarations) { - return declarations.map(declaration => { - const sourceFile = declaration.getSourceFile(); - return { - fileName: sourceFile.fileName, - span: {start: declaration.getStart(), end: declaration.getEnd()} - }; - }); - } -} - -function parentDeclarationOf(node: ts.Node): ts.Node|undefined { - while (node) { - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - return node; - case ts.SyntaxKind.SourceFile: - return undefined; - } - node = node.parent !; - } -} - -function getContainerOf(symbol: ts.Symbol, context: TypeContext): Symbol|undefined { - if (symbol.getFlags() & ts.SymbolFlags.ClassMember && symbol.declarations) { - for (const declaration of symbol.declarations) { - const parent = parentDeclarationOf(declaration); - if (parent) { - const type = context.checker.getTypeAtLocation(parent); - if (type) { - return new TypeWrapper(type, context); - } - } - } - } -} - -function getTypeParameterOf(type: ts.Type, name: string): ts.Type|undefined { - if (type && type.symbol && type.symbol.name == name) { - const typeArguments: ts.Type[] = (type as any).typeArguments; - if (typeArguments && typeArguments.length <= 1) { - return typeArguments[0]; - } - } -} - -function typeKindOf(type: ts.Type | undefined): BuiltinType { - if (type) { - if (type.flags & ts.TypeFlags.Any) { - return BuiltinType.Any; - } else if ( - type.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLike | ts.TypeFlags.StringLiteral)) { - return BuiltinType.String; - } else if (type.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLike)) { - return BuiltinType.Number; - } else if (type.flags & (ts.TypeFlags.Undefined)) { - return BuiltinType.Undefined; - } else if (type.flags & (ts.TypeFlags.Null)) { - return BuiltinType.Null; - } else if (type.flags & ts.TypeFlags.Union) { - // If all the constituent types of a union are the same kind, it is also that kind. - let candidate: BuiltinType|null = null; - const unionType = type as ts.UnionType; - if (unionType.types.length > 0) { - candidate = typeKindOf(unionType.types[0]); - for (const subType of unionType.types) { - if (candidate != typeKindOf(subType)) { - return BuiltinType.Other; - } - } - } - if (candidate != null) { - return candidate; - } - } else if (type.flags & ts.TypeFlags.TypeParameter) { - return BuiltinType.Unbound; - } - } - return BuiltinType.Other; -} - - -function getFromSymbolTable(symbolTable: ts.SymbolTable, key: string): ts.Symbol|undefined { - const table = symbolTable as any; - let symbol: ts.Symbol|undefined; - - if (typeof table.get === 'function') { - // TS 2.2 uses a Map - symbol = table.get(key); - } else { - // TS pre-2.2 uses an object - symbol = table[key]; - } - - return symbol; -} diff --git a/packages/language-service/src/utils.ts b/packages/language-service/src/utils.ts index dc751d66b5..425b9fbd6d 100644 --- a/packages/language-service/src/utils.ts +++ b/packages/language-service/src/utils.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveSummary, CompileTypeMetadata, CssSelector, ParseSourceSpan, identifierName} from '@angular/compiler'; +import {AstPath, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, Node as HtmlNode, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, identifierName, templateVisitAll, visitAll} from '@angular/compiler'; +import {DiagnosticTemplateInfo} from '@angular/compiler-cli'; import * as ts from 'typescript'; + import {SelectorInfo, TemplateInfo} from './common'; import {Span} from './types'; @@ -110,3 +112,68 @@ export function isTypescriptVersion(low: string, high?: string) { return true; } + +export function diagnosticInfoFromTemplateInfo(info: TemplateInfo): DiagnosticTemplateInfo { + return { + fileName: info.fileName, + offset: info.template.span.start, + query: info.template.query, + members: info.template.members, + htmlAst: info.htmlAst, + templateAst: info.templateAst + }; +} + +export function findTemplateAstAt( + ast: TemplateAst[], position: number, allowWidening: boolean = false): TemplateAstPath { + const path: TemplateAst[] = []; + const visitor = new class extends RecursiveTemplateAstVisitor { + visit(ast: TemplateAst, context: any): any { + let span = spanOf(ast); + if (inSpan(position, span)) { + const len = path.length; + if (!len || allowWidening || isNarrower(span, spanOf(path[len - 1]))) { + path.push(ast); + } + } else { + // Returning a value here will result in the children being skipped. + return true; + } + } + + visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { + return this.visitChildren(context, visit => { + // Ignore reference, variable and providers + visit(ast.attrs); + visit(ast.directives); + visit(ast.children); + }); + } + + visitElement(ast: ElementAst, context: any): any { + return this.visitChildren(context, visit => { + // Ingnore providers + visit(ast.attrs); + visit(ast.inputs); + visit(ast.outputs); + visit(ast.references); + visit(ast.directives); + visit(ast.children); + }); + } + + visitDirective(ast: DirectiveAst, context: any): any { + // Ignore the host properties of a directive + const result = this.visitChildren(context, visit => { visit(ast.inputs); }); + // We never care about the diretive itself, just its inputs. + if (path[path.length - 1] == ast) { + path.pop(); + } + return result; + } + }; + + templateVisitAll(visitor, ast); + + return new AstPath(path, position); +}