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

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