refactor(compiler-cli): Move diagnostics files to language service (#33809)

The following files are consumed only by the language service and do not
have to be in compiler-cli:

1. expression_diagnostics.ts
2. expression_type.ts
3. typescript_symbols.ts
4. symbols.ts

PR Close #33809
This commit is contained in:
Keen Yee Liau
2019-11-13 14:26:58 -08:00
committed by Alex Rickabaugh
parent 784fd26473
commit 9935aa43ad
19 changed files with 82 additions and 145 deletions

View File

@ -8,10 +8,6 @@
import {NodeJSFileSystem, setFileSystem} from './src/ngtsc/file_system';
export {AotCompilerHost, AotCompilerHost as StaticReflectorHost, StaticReflector, StaticSymbol} from '@angular/compiler';
export {DiagnosticTemplateInfo, getExpressionScope, getTemplateExpressionDiagnostics} from './src/diagnostics/expression_diagnostics';
export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expression_type';
export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './src/diagnostics/symbols';
export {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './src/diagnostics/typescript_symbols';
export {VERSION} from './src/version';
export * from './src/metadata';

View File

@ -1,363 +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 {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 getDefinitionOf(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 getDefinitionOf(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
}
}];
}
}
/**
* Resolve the specified `variable` from the `directives` list and return the
* corresponding symbol. If resolution fails, return the `any` type.
* @param variable template variable to resolve
* @param directives template context
* @param query
*/
function findSymbolForVariableInDirectives(
variable: VariableAst, directives: DirectiveAst[], query: SymbolQuery): Symbol {
for (const d of directives) {
// Get the symbol table for the directive's StaticSymbol
const table = query.getTemplateContext(d.directive.type.reference);
if (!table) {
continue;
}
const symbol = table.get(variable.value);
if (symbol) {
return symbol;
}
}
return query.getBuiltinType(BuiltinType.Any);
}
/**
* Resolve all variable declarations in a template by traversing the specified
* `path`.
* @param info
* @param path template AST path
*/
function getVarDeclarations(
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration[] {
const results: SymbolDeclaration[] = [];
for (let current = path.head; current; current = path.childOf(current)) {
if (!(current instanceof EmbeddedTemplateAst)) {
continue;
}
const {directives, variables} = current;
for (const variable of variables) {
let symbol = findSymbolForVariableInDirectives(variable, directives, info.query);
const kind = info.query.getTypeKind(symbol);
if (kind === BuiltinType.Any || kind === BuiltinType.Unbound) {
// For special cases such as ngFor and ngIf, the any type is not very useful.
// We can do better by resolving the binding value.
const symbolsInScope = info.query.mergeSymbolTable([
info.members,
// Since we are traversing the AST path from head to tail, any variables
// that have been declared so far are also in scope.
info.query.createSymbolTable(results),
]);
symbol = refinedVariableType(symbolsInScope, info.query, current);
}
results.push({
name: variable.name,
kind: 'variable',
type: symbol, get definition() { return getDefinitionOf(info, variable); },
});
}
}
return results;
}
/**
* Resolve a more specific type for the variable in `templateElement` by inspecting
* all variables that are in scope in the `mergedTable`. This function is a special
* case for `ngFor` and `ngIf`. If resolution fails, return the `any` type.
* @param mergedTable symbol table for all variables in scope
* @param query
* @param templateElement
*/
function refinedVariableType(
mergedTable: SymbolTable, query: SymbolQuery, 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(mergedTable, query, {}).getType(ngForOfBinding.value);
if (bindingType) {
const result = query.getElementType(bindingType);
if (result) {
return result;
}
}
}
}
// Special case the ngIf directive ( *ngIf="data$ | async as variable" )
const ngIfDirective =
templateElement.directives.find(d => identifierName(d.directive.type) === 'NgIf');
if (ngIfDirective) {
const ngIfBinding = ngIfDirective.inputs.find(i => i.directiveName === 'ngIf');
if (ngIfBinding) {
const bindingType = new AstType(mergedTable, query, {}).getType(ngIfBinding.value);
if (bindingType) {
return bindingType;
}
}
}
// We can't do better, return any
return query.getBuiltinType(BuiltinType.Any);
}
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;
// TODO(issue/24571): remove '!'.
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 define 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 references 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) {
return last.valueSpan.start.offset;
}
return ast.sourceSpan.start.offset;
}
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) {
const scope = this.getExpressionScope(this.path, includeEvent);
this.diagnostics.push(...getExpressionDiagnostics(scope, ast, this.info.query, {
event: includeEvent
}).map(d => ({
span: offsetSpan(d.ast.span, offset + this.info.offset),
kind: d.kind,
message: d.message
})));
}
private push(ast: TemplateAst) { this.path.push(ast); }
private pop() { this.path.pop(); }
private reportError(message: string, span: Span|undefined) {
if (span) {
this.diagnostics.push(
{span: offsetSpan(span, this.info.offset), kind: DiagnosticKind.Error, message});
}
}
private reportWarning(message: string, span: Span) {
this.diagnostics.push(
{span: offsetSpan(span, this.info.offset), kind: DiagnosticKind.Warning, message});
}
}
function hasTemplateReference(type: CompileTypeMetadata): boolean {
if (type.diDeps) {
for (let diDep of type.diDeps) {
if (diDep.token && diDep.token.identifier &&
identifierName(diDep.token !.identifier !) == 'TemplateRef')
return true;
}
}
return false;
}
function offsetSpan(span: Span, amount: number): Span {
return {start: span.start + amount, end: span.end + amount};
}
function spanOf(sourceSpan: ParseSourceSpan): Span {
return {start: sourceSpan.start.offset, end: sourceSpan.end.offset};
}

View File

@ -1,430 +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 {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, 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 {
// TODO(issue/24571): remove '!'.
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 getType = (ast: AST, operation: string): Symbol => {
const type = this.getType(ast);
if (type.nullable) {
switch (operation) {
case '&&':
case '||':
case '==':
case '!=':
case '===':
case '!==':
// Nullable allowed.
break;
default:
this.reportError(`The expression might be null`, ast);
break;
}
return this.query.getNonNullableType(type);
}
return type;
};
const leftType = getType(ast.left, ast.operation);
const rightType = getType(ast.right, ast.operation);
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.
if (this.diagnostics) {
visitAstChildren(ast, this);
}
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: '$implicit',
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) {
// If we are producing diagnostics, visit the children
if (this.diagnostics) {
visitAstChildren(ast, this);
}
// The type of a prefix ! is always boolean.
return this.query.getBuiltinType(BuiltinType.Boolean);
}
visitNonNullAssert(ast: NonNullAssert) {
const expressionType = this.getType(ast.expression);
return this.query.getNonNullableType(expressionType);
}
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);
}
// TODO(issue/24571): remove '!'.
private _anyType !: Symbol;
private get anyType(): Symbol {
let result = this._anyType;
if (!result) {
result = this._anyType = this.query.getBuiltinType(BuiltinType.Any);
}
return result;
}
// TODO(issue/24571): remove '!'.
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 == '$implicit') {
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 == '$implicit') {
receiverInfo = 'the component';
} else {
receiverInfo = `'${receiverInfo}'`;
}
this.reportWarning(
`Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast);
}
return member.type;
}
private reportError(message: string, ast: AST): Symbol {
if (this.diagnostics) {
this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Error, message, ast));
}
return this.anyType;
}
private reportWarning(message: string, ast: AST): Symbol {
if (this.diagnostics) {
this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Warning, message, ast));
}
return this.anyType;
}
private isAny(symbol: Symbol): boolean {
return !symbol || this.query.getTypeKind(symbol) == BuiltinType.Any ||
(!!symbol.type && this.isAny(symbol.type));
}
}

View File

@ -1,352 +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 {StaticSymbol} from '@angular/compiler';
/**
* The range of a span of text in a source file.
*
* @publicApi
*/
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.
*
* @publicApi
*/
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.
*
* @publicApi
*/
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.
*
* @publicApi
*/
export interface Signature {
/**
* The arguments of the signture. The order of `arguments.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.
*
* @publicApi
*/
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 definition.
*
* @publicApi
*/
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.
*
* @publicApi
*/
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.
*
* @publicApi
*/
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.
*
* @publicApi
*/
export type Pipes = PipeInfo[] | undefined;
/**
* Describes the language context in which an Angular expression is evaluated.
*
* @publicApi
*/
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|undefined;
/**
* Return the members that are in the context of a type's template reference.
*/
getTemplateContext(type: StaticSymbol): SymbolTable|undefined;
/**
* Produce a symbol table with the given symbols. Used to produce a symbol table
* for use with mergeSymbolTables().
*/
createSymbolTable(symbols: SymbolDeclaration[]): SymbolTable;
/**
* Produce a merged symbol table. If the symbol tables contain duplicate entries
* the entries of the latter symbol tables will obscure the entries in the prior
* symbol tables.
*
* The symbol tables passed to this routine MUST be produces by the same instance
* of SymbolQuery that is being called.
*/
mergeSymbolTable(symbolTables: SymbolTable[]): SymbolTable;
/**
* Return the span of the narrowest non-token node at the given location.
*/
getSpanAt(line: number, column: number): Span|undefined;
}

View File

@ -1,825 +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 {CompilePipeSummary, StaticSymbol} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {BuiltinType, DeclarationKind, Definition, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols';
import {isVersionBetween} from './typescript_version';
// 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);
if (node) {
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>();
// TODO(issue/24571): remove '!'.
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|undefined {
const context: TypeContext = {node: this.source, program: this.program, checker: this.checker};
const typeSymbol = findClassSymbolInContext(type, context);
return typeSymbol && 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 && 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>';
}
public readonly kind: DeclarationKind = 'type';
public readonly language: string = 'typescript';
public readonly type: Symbol|undefined = undefined;
public readonly container: Symbol|undefined = undefined;
public readonly public: boolean = true;
get callable(): boolean { return typeCallable(this.tsType); }
get nullable(): boolean {
return this.context.checker.getNonNullableType(this.tsType) != this.tsType;
}
get definition(): Definition|undefined {
const symbol = this.tsType.getSymbol();
return symbol ? definitionFromTsSymbol(symbol) : undefined;
}
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;
// TODO(issue/24571): remove '!'.
private _tsType !: ts.Type;
// TODO(issue/24571): remove '!'.
private _members !: SymbolTable;
public readonly nullable: boolean = false;
public readonly language: string = 'typescript';
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 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 {
public readonly language: string = 'ng-template';
public readonly nullable: boolean = false;
public readonly public: boolean = true;
constructor(private declaration: SymbolDeclaration) {}
get name() { return this.declaration.name; }
get kind() { return this.declaration.kind; }
get container(): Symbol|undefined { return undefined; }
get type() { return this.declaration.type; }
get callable(): boolean { return this.declaration.type.callable; }
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; }
}
/**
* Indicates the lower bound TypeScript version supporting `SymbolTable` as an ES6 `Map`.
* For lower versions, `SymbolTable` is implemented as a dictionary
*/
const MIN_TS_VERSION_SUPPORTING_MAP = '2.2';
export const toSymbolTableFactory = (tsVersion: string) => (symbols: ts.Symbol[]) => {
if (isVersionBetween(tsVersion, MIN_TS_VERSION_SUPPORTING_MAP)) {
// ∀ Typescript version >= 2.2, `SymbolTable` is implemented as an ES6 `Map`
const result = new Map<string, ts.Symbol>();
for (const symbol of symbols) {
result.set(symbol.name, symbol);
}
// First, tell the compiler that `result` is of type `any`. Then, use a second type assertion
// to `ts.SymbolTable`.
// Otherwise, `Map<string, ts.Symbol>` and `ts.SymbolTable` will be considered as incompatible
// types by the compiler
return <ts.SymbolTable>(<any>result);
}
// ∀ Typescript version < 2.2, `SymbolTable` is implemented as a dictionary
const result: {[name: string]: ts.Symbol} = {};
for (const symbol of symbols) {
result[symbol.name] = symbol;
}
return <ts.SymbolTable>(<any>result);
};
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;
const toSymbolTable = toSymbolTableFactory(ts.version);
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)); }
}
// This matches .d.ts files that look like ".../<package-name>/<package-name>.d.ts",
const INDEX_PATTERN = /[\\/]([^\\/]+)[\\/]\1\.d\.ts$/;
class PipeSymbol implements Symbol {
// TODO(issue/24571): remove '!'.
private _tsType !: ts.Type;
public readonly kind: DeclarationKind = 'pipe';
public readonly language: string = 'typescript';
public readonly container: Symbol|undefined = undefined;
public readonly callable: boolean = true;
public readonly nullable: boolean = false;
public readonly public: boolean = true;
constructor(private pipe: CompilePipeSummary, private context: TypeContext) {}
get name(): string { return this.pipe.name; }
get type(): Symbol|undefined { return new TypeWrapper(this.tsType, this.context); }
get definition(): Definition|undefined {
const symbol = this.tsType.getSymbol();
return symbol ? definitionFromTsSymbol(symbol) : undefined;
}
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 > 0) {
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 = parameterType.tsType;
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 {
let sourceFile = context.program.getSourceFile(type.filePath);
if (!sourceFile) {
// This handles a case where an <packageName>/index.d.ts and a <packageName>/<packageName>.d.ts
// are in the same directory. If we are looking for <packageName>/<packageName> and didn't
// find it, look for <packageName>/index.d.ts as the program might have found that instead.
const p = type.filePath;
const m = p.match(INDEX_PATTERN);
if (m) {
const indexVersion = path.join(path.dirname(p), 'index.d.ts');
sourceFile = context.program.getSourceFile(indexVersion);
}
}
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 {
public readonly size: number = 0;
get(key: string): Symbol|undefined { return undefined; }
has(key: string): boolean { return false; }
values(): Symbol[] { return []; }
static instance = new EmptyTable();
}
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.LiteralLikeNode>{
kind: ts.SyntaxKind.NumericLiteral,
text: node.getText(),
};
setParents(<any>{kind: ts.SyntaxKind.ExpressionStatement, expression: numeric}, node);
type = checker.getTypeAtLocation(numeric);
break;
case BuiltinType.String:
type = checker.getTypeAtLocation(setParents(
<ts.LiteralLikeNode>{
kind: ts.SyntaxKind.NoSubstitutionTemplateLiteral,
text: node.getText(),
},
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 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;
}

View File

@ -14,10 +14,6 @@ Angular modules and Typescript as this will indirectly add a dependency
to the language service.
*/
export {DiagnosticTemplateInfo, ExpressionDiagnostic, getExpressionDiagnostics, getExpressionScope, getTemplateExpressionDiagnostics} from './diagnostics/expression_diagnostics';
export {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './diagnostics/expression_type';
export {BuiltinType, DeclarationKind, Definition, Location, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './diagnostics/symbols';
export {getClassFromStaticSymbol, getClassMembers, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './diagnostics/typescript_symbols';
export {MetadataCollector, ModuleMetadata} from './metadata';
export {CompilerOptions} from './transformers/api';
export {MetadataReaderCache, MetadataReaderHost, createMetadataReaderCache, readMetadata} from './transformers/metadata_reader';

View File

@ -1,28 +1,11 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
ts_library(
name = "mocks",
testonly = True,
srcs = [
"mocks.ts",
],
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/test:test_utils",
"//packages/core",
"@npm//typescript",
],
)
# check_types_spec
ts_library(
name = "check_types_lib",
testonly = True,
srcs = ["check_types_spec.ts"],
deps = [
":mocks",
"//packages/compiler-cli",
"//packages/compiler-cli/test:test_utils",
"@npm//typescript",
@ -49,69 +32,6 @@ jasmine_node_test(
],
)
# expression_diagnostics_spec
ts_library(
name = "expression_diagnostics_lib",
testonly = True,
srcs = ["expression_diagnostics_spec.ts"],
deps = [
":mocks",
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/test:test_utils",
"//packages/language-service",
"@npm//typescript",
],
)
jasmine_node_test(
name = "expression_diagnostics",
bootstrap = ["angular/tools/testing/init_node_spec.js"],
data = [
"//packages/common:npm_package",
"//packages/core:npm_package",
"//packages/forms:npm_package",
],
tags = [
# Disabled as these tests pertain to diagnostics in the old ngc compiler. The Ivy ngtsc
# compiler has its own tests for diagnostics.
"no-ivy-aot",
],
deps = [
":expression_diagnostics_lib",
"//packages/core",
"//tools/testing:node",
],
)
# typescript_symbols_spec
ts_library(
name = "typescript_symbols_lib",
testonly = True,
srcs = ["typescript_symbols_spec.ts"],
deps = [
":mocks",
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/test:test_utils",
"//packages/compiler/test:test_utils",
"//packages/language-service",
"@npm//typescript",
],
)
jasmine_node_test(
name = "typescript_symbols",
bootstrap = ["angular/tools/testing/init_node_spec.js"],
data = [
],
deps = [
":typescript_symbols_lib",
"//packages/core",
"//tools/testing:node",
],
)
# typescript_version_spec
ts_library(
name = "typescript_version_lib",

View File

@ -1,254 +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 {StaticSymbol} from '@angular/compiler';
import {ReflectorHost} from '@angular/language-service/src/reflector_host';
import * as ts from 'typescript';
import {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 service: ts.LanguageService;
let context: DiagnosticContext;
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 symbolResolverHost = new ReflectorHost(() => program !, host);
context = new DiagnosticContext(service, program !, checker, symbolResolverHost);
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 + messageText.next.map(messageToString);
return messageText.messageText;
}
}
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
if (diagnostics && diagnostics.length) {
const message =
'messages: ' + 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) {
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 reject *ngIf of misspelled identifier in PrefixNot node',
() =>
reject('<div *ngIf="people && !persson"></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 accept an async *ngIf', () => accept(`
<div *ngIf="promised_person | async as p">
{{p.name.first}} {{p.name.last}}
</div>
`));
it('should reject misspelled field in async *ngIf', () => reject(
`
<div *ngIf="promised_person | async as p">
{{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 type assert 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'));
describe('with comparisons between nullable and non-nullable', () => {
it('should accept ==', () => accept(`<div>{{e == 1 ? 'a' : 'b'}}</div>`));
it('should accept ===', () => accept(`<div>{{e === 1 ? 'a' : 'b'}}</div>`));
it('should accept !=', () => accept(`<div>{{e != 1 ? 'a' : 'b'}}</div>`));
it('should accept !==', () => accept(`<div>{{e !== 1 ? 'a' : 'b'}}</div>`));
it('should accept &&', () => accept(`<div>{{e && 1 ? 'a' : 'b'}}</div>`));
it('should accept ||', () => accept(`<div>{{e || 1 ? 'a' : 'b'}}</div>`));
it('should reject >',
() => reject(`<div>{{e > 1 ? 'a' : 'b'}}</div>`, 'The expression might be null'));
});
});
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[];
e?: number;
getPerson(): Person { return this.person; }
click() {}
}
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [AppComponent]
})
export class AppModule {}
`
}
}
};

View File

@ -1,253 +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 {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver} 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 {DiagnosticTemplateInfo} from '../../src/diagnostics/expression_diagnostics';
import {getClassMembers, getPipesTable, getSymbolQuery} from '../../src/diagnostics/typescript_symbols';
import {Directory, MockAotContext} from '../mocks';
import {setup} from '../test_support';
const realFiles = new Map<string, string>();
export class MockLanguageServiceHost implements ts.LanguageServiceHost {
private options: ts.CompilerOptions;
private context: MockAotContext;
private assumedExist = new Set<string>();
constructor(private scripts: string[], files: Directory, currentDirectory: string = '/') {
const support = setup();
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/*': [path.join(support.basePath, 'node_modules/@angular/*')]}
};
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.posix.dirname(ts.getDefaultLibFilePath(this.getCompilationSettings()));
fileName = path.posix.join(libPath, basename);
}
if (fileName.startsWith('app/')) {
fileName = path.posix.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; },
toSummaryFileName(sourceFilePath: string) { return sourceFilePath; },
fromSummaryFileName(filePath: string): string{return filePath;},
},
staticSymbolCache);
export class DiagnosticContext {
// tslint:disable
// TODO(issue/24571): remove '!'.
_analyzedModules !: NgAnalyzedModules;
_staticSymbolResolver: StaticSymbolResolver|undefined;
_reflector: StaticReflector|undefined;
_errors: {e: any, path?: string}[] = [];
_resolver: CompileMetadataResolver|undefined;
// tslint:enable
constructor(
public service: ts.LanguageService, public program: ts.Program,
public checker: ts.TypeChecker, public host: StaticSymbolResolverHost) {}
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 !));
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(): 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, htmlParser, 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 programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
analyzedModules = this._analyzedModules =
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, 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, context.reflector, expressionParser, new DomElementSchemaRegistry(), htmlParser,
null !, []);
const htmlResult = htmlParser.parse(template, '', {tokenizeExpansionForms: 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);
if (sourceFile) {
const query = getSymbolQuery(
context.program, context.checker, sourceFile,
() => getPipesTable(
sourceFile, context.program, context.checker, compiledTemplate.pipes));
return {
fileName: templateFile,
offset: 0, query, members,
htmlAst: compiledTemplate.htmlAst,
templateAst: compiledTemplate.templateAst
};
}
}
}
}
function removeMissing<T>(values: (T | null | undefined)[]): T[] {
return values.filter(e => !!e) as T[];
}

View File

@ -1,139 +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 {ReflectorHost} from '@angular/language-service/src/reflector_host';
import * as ts from 'typescript';
import {BuiltinType, Symbol, SymbolQuery, SymbolTable} from '../../src/diagnostics/symbols';
import {getSymbolQuery, toSymbolTableFactory} from '../../src/diagnostics/typescript_symbols';
import {Directory} from '../mocks';
import {DiagnosticContext, MockLanguageServiceHost} from './mocks';
function emptyPipes(): SymbolTable {
return {
size: 0,
get(key: string) { return undefined; },
has(key: string) { return false; },
values(): Symbol[]{return [];}
};
}
describe('symbol query', () => {
let program: ts.Program;
let checker: ts.TypeChecker;
let sourceFile: ts.SourceFile;
let query: SymbolQuery;
let context: DiagnosticContext;
beforeEach(() => {
const registry = ts.createDocumentRegistry(false, '/src');
const host = new MockLanguageServiceHost(
['/quickstart/app/app.component.ts'], QUICKSTART, '/quickstart');
const service = ts.createLanguageService(host, registry);
program = service.getProgram() !;
checker = program.getTypeChecker();
sourceFile = program.getSourceFile('/quickstart/app/app.component.ts') !;
const symbolResolverHost = new ReflectorHost(() => program, host);
context = new DiagnosticContext(service, program, checker, symbolResolverHost);
query = getSymbolQuery(program, checker, sourceFile, emptyPipes);
});
it('should be able to get undefined for an unknown symbol', () => {
const unknownType = context.getStaticSymbol('/unkonwn/file.ts', 'UnknownType');
const symbol = query.getTypeSymbol(unknownType);
expect(symbol).toBeUndefined();
});
it('should return correct built-in types', () => {
const tests: Array<[BuiltinType, boolean, ts.TypeFlags?]> = [
// builtinType, throws, want
[BuiltinType.Any, false, ts.TypeFlags.Any],
[BuiltinType.Boolean, false, ts.TypeFlags.BooleanLiteral],
[BuiltinType.Null, false, ts.TypeFlags.Null],
[BuiltinType.Number, false, ts.TypeFlags.NumberLiteral],
[BuiltinType.String, false, ts.TypeFlags.StringLiteral],
[BuiltinType.Undefined, false, ts.TypeFlags.Undefined],
[BuiltinType.Unbound, true],
[BuiltinType.Other, true],
];
for (const [builtinType, throws, want] of tests) {
if (throws) {
expect(() => query.getBuiltinType(builtinType)).toThrow();
} else {
const symbol = query.getBuiltinType(builtinType);
const got: ts.TypeFlags = (symbol as any).tsType.flags;
expect(got).toBe(want !);
}
}
});
});
describe('toSymbolTableFactory(tsVersion)', () => {
it('should return a Map for versions of TypeScript >= 2.2 and a dictionary otherwise', () => {
const a = { name: 'a' } as ts.Symbol;
const b = { name: 'b' } as ts.Symbol;
expect(toSymbolTableFactory('2.1')([a, b]) instanceof Map).toEqual(false);
expect(toSymbolTableFactory('2.4')([a, b]) instanceof Map).toEqual(true);
// Check that for the lower bound version `2.2`, toSymbolTableFactory('2.2') returns a map
expect(toSymbolTableFactory('2.2')([a, b]) instanceof Map).toEqual(true);
});
});
function appComponentSource(template: string): string {
return `
import {Component} from '@angular/core';
export interface Person {
name: string;
address: Address;
}
export interface Address {
street: string;
city: string;
state: string;
zip: string;
}
@Component({
template: '${template}'
})
export class AppComponent {
name = 'Angular';
person: Person;
people: Person[];
maybePerson?: Person;
getName(): string { return this.name; }
getPerson(): Person { return this.person; }
getMaybePerson(): Person | undefined { this.maybePerson; }
}
`;
}
const QUICKSTART: Directory = {
quickstart: {
app: {
'app.component.ts': appComponentSource('<h1>Hello {{name}}</h1>'),
'app.module.ts': `
import { NgModule } from '@angular/core';
import { toString } from './utils';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
}
}
};