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:

committed by
Jason Aden

parent
9e661e58d1
commit
bb0902c592
325
packages/compiler-cli/src/diagnostics/expression_diagnostics.ts
Normal file
325
packages/compiler-cli/src/diagnostics/expression_diagnostics.ts
Normal 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};
|
||||
}
|
394
packages/compiler-cli/src/diagnostics/expression_type.ts
Normal file
394
packages/compiler-cli/src/diagnostics/expression_type.ts
Normal 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));
|
||||
}
|
||||
}
|
352
packages/compiler-cli/src/diagnostics/symbols.ts
Normal file
352
packages/compiler-cli/src/diagnostics/symbols.ts
Normal 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;
|
||||
}
|
861
packages/compiler-cli/src/diagnostics/typescript_symbols.ts
Normal file
861
packages/compiler-cli/src/diagnostics/typescript_symbols.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user