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.
This commit is contained in:
Chuck Jazdzewski 2017-05-09 16:16:50 -07:00 committed by Jason Aden
parent 9e661e58d1
commit bb0902c592
23 changed files with 2891 additions and 2270 deletions

View File

@ -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';

View File

@ -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<T> or EventEmitter<T>
// 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<TemplateAst>([]);
}
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};
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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<BuiltinType, Symbol>();
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) || '<anonymous>';
}
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<string, ts.Symbol>();
for (const symbol of symbols) {
result.set(symbol.name, symbol);
}
return <ts.SymbolTable>(result as any);
}) :
(symbols => {
const result = <any>{};
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<string, Symbol>();
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(
<ts.Node><any>{
kind: ts.SyntaxKind.AsExpression,
expression: <ts.Node>{kind: ts.SyntaxKind.TrueKeyword},
type: <ts.Node>{kind: ts.SyntaxKind.AnyKeyword}
},
node));
break;
case BuiltinType.Boolean:
type =
checker.getTypeAtLocation(setParents(<ts.Node>{kind: ts.SyntaxKind.TrueKeyword}, node));
break;
case BuiltinType.Null:
type =
checker.getTypeAtLocation(setParents(<ts.Node>{kind: ts.SyntaxKind.NullKeyword}, node));
break;
case BuiltinType.Number:
const numeric = <ts.Node>{kind: ts.SyntaxKind.NumericLiteral};
setParents(<any>{kind: ts.SyntaxKind.ExpressionStatement, expression: numeric}, node);
type = checker.getTypeAtLocation(numeric);
break;
case BuiltinType.String:
type = checker.getTypeAtLocation(
setParents(<ts.Node>{kind: ts.SyntaxKind.NoSubstitutionTemplateLiteral}, node));
break;
case BuiltinType.Undefined:
type = checker.getTypeAtLocation(setParents(
<ts.Node><any>{
kind: ts.SyntaxKind.VoidExpression,
expression: <ts.Node>{kind: ts.SyntaxKind.NumericLiteral}
},
node));
break;
default:
throw new Error(`Internal error, unhandled literal kind ${kind}:${BuiltinType[kind]}`);
}
return type;
}
function setParents<T extends ts.Node>(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;
}

View File

@ -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('<div *ngIf="person">{{person.name.first}}</div>'));
it('should reject *ngIf of misspelled identifier',
() => reject(
'<div *ngIf="persson">{{person.name.first}}</div>',
'Identifier \'persson\' is not defined'));
it('should accept an *ngFor', () => accept(`
<div *ngFor="let p of people">
{{p.name.first}} {{p.name.last}}
</div>
`));
it('should reject misspelled field in *ngFor', () => reject(
`
<div *ngFor="let p of people">
{{p.names.first}} {{p.name.last}}
</div>
`,
'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(`
<div *ngFor="let p of promised_people | async">
{{p.name.first}} {{p.name.last}}
</div>
`));
it('should reject misspelled field an async *ngFor', () => reject(
`
<div *ngFor="let p of promised_people | async">
{{p.name.first}} {{p.nume.last}}
</div>
`,
'Identifier \'nume\' is not defined'));
it('should reject access to potentially undefined field',
() => reject(`<div>{{maybe_person.name.first}}`, 'The expression might be null'));
it('should accept a safe accss to an undefined field',
() => accept(`<div>{{maybe_person?.name.first}}</div>`));
it('should accept a # reference', () => accept(`
<form #f="ngForm" novalidate>
<input name="first" ngModel required #first="ngModel">
<input name="last" ngModel>
<button>Submit</button>
</form>
<p>First name value: {{ first.value }}</p>
<p>First name valid: {{ first.valid }}</p>
<p>Form value: {{ f.value | json }}</p>
<p>Form valid: {{ f.valid }}</p>
`));
it('should reject a misspelled field of a # reference',
() => reject(
`
<form #f="ngForm" novalidate>
<input name="first" ngModel required #first="ngModel">
<input name="last" ngModel>
<button>Submit</button>
</form>
<p>First name value: {{ first.valwe }}</p>
<p>First name valid: {{ first.valid }}</p>
<p>Form value: {{ f.value | json }}</p>
<p>Form valid: {{ f.valid }}</p>
`,
'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('<div (click)="click($event)">{{person.name.first}}</div>'));
it('should reject a misspelled event handler',
() => reject(
'<div (click)="clack($event)">{{person.name.first}}</div>', 'Unknown method \'clack\''));
it('should reject an uncalled event handler',
() => reject(
'<div (click)="click">{{person.name.first}}</div>', '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<Person>;
promised_people: Promise<Person[]>;
private private_person: Person;
private private_people: Person[];
getPerson(): Person { return this.person; }
click() {}
}
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [AppComponent]
})
export class AppModule {}
`
}
}
};

View File

@ -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<string, string>();
export class MockLanguageServiceHost implements ts.LanguageServiceHost, CompilerHostContext {
private options: ts.CompilerOptions;
private context: MockAotContext;
private assumedExist = new Set<string>();
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<string> { 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<string> { 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<T>(values: (T | null | undefined)[]): T[] {
return values.filter(e => !!e) as T[];
}

View File

@ -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<T> {
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]; }

View File

@ -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';

View File

@ -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.

View File

@ -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<T extends AST>(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); },
});
}

View File

@ -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<T extends Node>(
context: any, cb: (visit: (<V extends Node>(children: V[]|undefined) => void)) => void) {
let results: any[][] = [];
let t = this;
function visit<T extends Node>(children: T[] | undefined) {
if (children) results.push(visitAll(t, children, context));
}
cb(visit);
return [].concat.apply([], results);
}
}
export type HtmlAstPath = AstPath<Node>;
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<Node>(path, position);
}

View File

@ -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<T extends TemplateAst>(
context: any,
cb: (visit: (<V extends TemplateAst>(children: V[]|undefined) => void)) => void) {
let results: any[][] = [];
let t = this;
function visit<T extends TemplateAst>(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<TemplateAst>;

View File

@ -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<HtmlAst>): 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<HtmlAst>): 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<HtmlAst>): Completions|
undefined {
let tail = path.tail;
if (tail instanceof Text) {

View File

@ -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
});
}
}

View File

@ -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<AST>;
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<AST>(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<AST> {
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<T extends AST>(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<T> or EventEmitter<T>
// 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
}
}];
}
}

View File

@ -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<Node> {
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<T extends Node>(
context: any, cb: (visit: (<V extends Node>(children: V[]|undefined) => void)) => void) {
const visitor = this.visitor || this;
let results: any[][] = [];
function visit<T extends Node>(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; }
}

View File

@ -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 {

View File

@ -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(); }

View File

@ -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<TemplateAst> {
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<T extends TemplateAst>(
context: any,
cb: (visit: (<V extends TemplateAst>(children: V[]|undefined) => void)) => void) {
const visitor = this.visitor || this;
let results: any[][] = [];
function visit<T extends TemplateAst>(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; }
}

View File

@ -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[];
}

View File

@ -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(<ts.Node><any>{
kind: ts.SyntaxKind.AsExpression,
expression: <ts.Node>{kind: ts.SyntaxKind.TrueKeyword},
type: <ts.Node>{kind: ts.SyntaxKind.AnyKeyword}
});
break;
case BuiltinType.Boolean:
type = checker.getTypeAtLocation(<ts.Node>{kind: ts.SyntaxKind.TrueKeyword});
break;
case BuiltinType.Null:
type = checker.getTypeAtLocation(<ts.Node>{kind: ts.SyntaxKind.NullKeyword});
break;
case BuiltinType.Number:
type = checker.getTypeAtLocation(<ts.Node>{kind: ts.SyntaxKind.NumericLiteral});
break;
case BuiltinType.String:
type =
checker.getTypeAtLocation(<ts.Node>{kind: ts.SyntaxKind.NoSubstitutionTemplateLiteral});
break;
case BuiltinType.Undefined:
type = checker.getTypeAtLocation(<ts.Node>{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<BuiltinType, Symbol>();
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) || '<anonymous>';
}
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<string, ts.Symbol>();
for (const symbol of symbols) {
result.set(symbol.name, symbol);
}
return <ts.SymbolTable>(result as any);
}
const result = <any>{};
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<string, Symbol>();
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(
<ts.Node><any>{
kind: ts.SyntaxKind.AsExpression,
expression: <ts.Node>{kind: ts.SyntaxKind.TrueKeyword},
type: <ts.Node>{kind: ts.SyntaxKind.AnyKeyword}
},
node));
break;
case BuiltinType.Boolean:
type =
checker.getTypeAtLocation(setParents(<ts.Node>{kind: ts.SyntaxKind.TrueKeyword}, node));
break;
case BuiltinType.Null:
type =
checker.getTypeAtLocation(setParents(<ts.Node>{kind: ts.SyntaxKind.NullKeyword}, node));
break;
case BuiltinType.Number:
const numeric = <ts.Node>{kind: ts.SyntaxKind.NumericLiteral};
setParents(<any>{kind: ts.SyntaxKind.ExpressionStatement, expression: numeric}, node);
type = checker.getTypeAtLocation(numeric);
break;
case BuiltinType.String:
type = checker.getTypeAtLocation(
setParents(<ts.Node>{kind: ts.SyntaxKind.NoSubstitutionTemplateLiteral}, node));
break;
case BuiltinType.Undefined:
type = checker.getTypeAtLocation(setParents(
<ts.Node><any>{
kind: ts.SyntaxKind.VoidExpression,
expression: <ts.Node>{kind: ts.SyntaxKind.NumericLiteral}
},
node));
break;
default:
throw new Error(`Internal error, unhandled literal kind ${kind}:${BuiltinType[kind]}`);
}
return type;
}
function setParents<T extends ts.Node>(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;
}

View File

@ -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<TemplateAst>(path, position);
}