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