2017-05-09 16:16:50 -07:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2019-12-16 12:11:39 -08:00
|
|
|
import {AST, AstPath, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveSummary, CompileTypeMetadata, DirectiveAst, ElementAst, EmbeddedTemplateAst, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstPath, VariableAst, identifierName, templateVisitAll, tokenReference} from '@angular/compiler';
|
2019-11-27 16:31:00 -08:00
|
|
|
import * as ts from 'typescript';
|
2017-05-09 16:16:50 -07:00
|
|
|
|
2019-11-27 16:31:00 -08:00
|
|
|
import {AstType, ExpressionDiagnosticsContext, TypeDiagnostic} from './expression_type';
|
2017-05-09 16:16:50 -07:00
|
|
|
import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols';
|
2019-11-27 16:31:00 -08:00
|
|
|
import {Diagnostic} from './types';
|
2019-12-26 13:26:54 -06:00
|
|
|
import {findOutputBinding, getPathToNodeAtPosition} from './utils';
|
2017-05-09 16:16:50 -07:00
|
|
|
|
|
|
|
export interface DiagnosticTemplateInfo {
|
|
|
|
fileName?: string;
|
|
|
|
offset: number;
|
|
|
|
query: SymbolQuery;
|
|
|
|
members: SymbolTable;
|
|
|
|
htmlAst: Node[];
|
|
|
|
templateAst: TemplateAst[];
|
|
|
|
}
|
|
|
|
|
2019-11-27 16:31:00 -08:00
|
|
|
export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo): Diagnostic[] {
|
2017-05-09 16:16:50 -07:00
|
|
|
const visitor = new ExpressionDiagnosticsVisitor(
|
2019-12-26 08:44:14 -06:00
|
|
|
info, (path: TemplateAstPath) => getExpressionScope(info, path));
|
2017-05-09 16:16:50 -07:00
|
|
|
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),
|
2017-08-02 11:20:07 -07:00
|
|
|
get definition() { return getDefinitionOf(info, reference); }
|
2017-05-09 16:16:50 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-08-02 11:20:07 -07:00
|
|
|
function getDefinitionOf(info: DiagnosticTemplateInfo, ast: TemplateAst): Definition|undefined {
|
2017-05-09 16:16:50 -07:00
|
|
|
if (info.fileName) {
|
|
|
|
const templateOffset = info.offset;
|
|
|
|
return [{
|
|
|
|
fileName: info.fileName,
|
|
|
|
span: {
|
|
|
|
start: ast.sourceSpan.start.offset + templateOffset,
|
|
|
|
end: ast.sourceSpan.end.offset + templateOffset
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-06 10:32:45 -08:00
|
|
|
/**
|
|
|
|
* Resolve all variable declarations in a template by traversing the specified
|
|
|
|
* `path`.
|
|
|
|
* @param info
|
|
|
|
* @param path template AST path
|
|
|
|
*/
|
2017-05-09 16:16:50 -07:00
|
|
|
function getVarDeclarations(
|
|
|
|
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration[] {
|
2019-11-06 10:32:45 -08:00
|
|
|
const results: SymbolDeclaration[] = [];
|
|
|
|
for (let current = path.head; current; current = path.childOf(current)) {
|
|
|
|
if (!(current instanceof EmbeddedTemplateAst)) {
|
|
|
|
continue;
|
|
|
|
}
|
2019-11-12 17:40:56 -08:00
|
|
|
for (const variable of current.variables) {
|
|
|
|
let symbol = info.members.get(variable.value) || info.query.getBuiltinType(BuiltinType.Any);
|
2019-11-06 10:32:45 -08:00
|
|
|
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),
|
|
|
|
]);
|
2019-11-27 11:29:37 -06:00
|
|
|
symbol = refinedVariableType(variable.value, symbolsInScope, info.query, current);
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
2019-11-06 10:32:45 -08:00
|
|
|
results.push({
|
|
|
|
name: variable.name,
|
|
|
|
kind: 'variable',
|
|
|
|
type: symbol, get definition() { return getDefinitionOf(info, variable); },
|
|
|
|
});
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
}
|
2019-11-06 10:32:45 -08:00
|
|
|
return results;
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
|
2019-11-27 11:29:37 -06:00
|
|
|
/**
|
|
|
|
* Gets the type of an ngFor exported value, as enumerated in
|
|
|
|
* https://angular.io/api/common/NgForOfContext
|
|
|
|
* @param value exported value name
|
|
|
|
* @param query type symbol query
|
|
|
|
*/
|
|
|
|
function getNgForExportedValueType(value: string, query: SymbolQuery): Symbol|undefined {
|
|
|
|
switch (value) {
|
|
|
|
case 'index':
|
|
|
|
case 'count':
|
|
|
|
return query.getBuiltinType(BuiltinType.Number);
|
|
|
|
case 'first':
|
|
|
|
case 'last':
|
|
|
|
case 'even':
|
|
|
|
case 'odd':
|
|
|
|
return query.getBuiltinType(BuiltinType.Boolean);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-06 10:32:45 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
2019-11-27 11:29:37 -06:00
|
|
|
* @param value variable value name
|
2019-11-06 10:32:45 -08:00
|
|
|
* @param mergedTable symbol table for all variables in scope
|
|
|
|
* @param query
|
|
|
|
* @param templateElement
|
|
|
|
*/
|
2017-05-09 16:16:50 -07:00
|
|
|
function refinedVariableType(
|
2019-11-27 11:29:37 -06:00
|
|
|
value: string, mergedTable: SymbolTable, query: SymbolQuery,
|
|
|
|
templateElement: EmbeddedTemplateAst): Symbol {
|
2017-05-09 16:16:50 -07:00
|
|
|
// 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) {
|
2019-11-27 11:29:37 -06:00
|
|
|
// Check if the variable value is a type exported by the ngFor statement.
|
|
|
|
let result = getNgForExportedValueType(value, query);
|
|
|
|
|
|
|
|
// Otherwise, check if there is a known type for the ngFor binding.
|
2019-11-06 10:32:45 -08:00
|
|
|
const bindingType = new AstType(mergedTable, query, {}).getType(ngForOfBinding.value);
|
2019-11-27 11:29:37 -06:00
|
|
|
if (!result && bindingType) {
|
|
|
|
result = query.getElementType(bindingType);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
return result;
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-05 15:18:05 +03:00
|
|
|
// 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) {
|
2019-11-06 10:32:45 -08:00
|
|
|
const bindingType = new AstType(mergedTable, query, {}).getType(ngIfBinding.value);
|
2019-10-05 15:18:05 +03:00
|
|
|
if (bindingType) {
|
|
|
|
return bindingType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-19 10:07:54 -07:00
|
|
|
// We can't do better, return any
|
2019-11-06 10:32:45 -08:00
|
|
|
return query.getBuiltinType(BuiltinType.Any);
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
|
2019-12-26 13:26:54 -06:00
|
|
|
function getEventDeclaration(
|
|
|
|
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration|undefined {
|
|
|
|
const event = path.tail;
|
|
|
|
if (!(event instanceof BoundEventAst)) {
|
|
|
|
// No event available in this context.
|
|
|
|
return;
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
2019-12-26 13:26:54 -06:00
|
|
|
|
|
|
|
const genericEvent: SymbolDeclaration = {
|
|
|
|
name: '$event',
|
|
|
|
kind: 'variable',
|
|
|
|
type: info.query.getBuiltinType(BuiltinType.Any),
|
|
|
|
};
|
|
|
|
|
|
|
|
const outputSymbol = findOutputBinding(event, path, info.query);
|
|
|
|
if (!outputSymbol) {
|
|
|
|
// The `$event` variable doesn't belong to an output, so its type can't be refined.
|
|
|
|
// TODO: type `$event` variables in bindings to DOM events.
|
|
|
|
return genericEvent;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The raw event type is wrapped in a generic, like EventEmitter<T> or Observable<T>.
|
|
|
|
const ta = outputSymbol.typeArguments();
|
|
|
|
if (!ta || ta.length !== 1) return genericEvent;
|
|
|
|
const eventType = ta[0];
|
|
|
|
|
|
|
|
return {...genericEvent, type: eventType};
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
|
2019-12-26 13:26:54 -06:00
|
|
|
/**
|
|
|
|
* Returns the symbols available in a particular scope of a template.
|
|
|
|
* @param info parsed template information
|
|
|
|
* @param path path of template nodes narrowing to the context the expression scope should be
|
|
|
|
* derived for.
|
|
|
|
*/
|
2017-05-09 16:16:50 -07:00
|
|
|
export function getExpressionScope(
|
2019-12-26 08:44:14 -06:00
|
|
|
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolTable {
|
2017-05-09 16:16:50 -07:00
|
|
|
let result = info.members;
|
|
|
|
const references = getReferences(info);
|
|
|
|
const variables = getVarDeclarations(info, path);
|
2019-12-26 13:26:54 -06:00
|
|
|
const event = getEventDeclaration(info, path);
|
|
|
|
if (references.length || variables.length || event) {
|
2017-05-09 16:16:50 -07:00
|
|
|
const referenceTable = info.query.createSymbolTable(references);
|
|
|
|
const variableTable = info.query.createSymbolTable(variables);
|
2019-12-26 13:26:54 -06:00
|
|
|
const eventsTable = info.query.createSymbolTable(event ? [event] : []);
|
2017-05-09 16:16:50 -07:00
|
|
|
result = info.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
|
|
|
|
private path: TemplateAstPath;
|
2020-01-14 22:48:13 -08:00
|
|
|
private directiveSummary: CompileDirectiveSummary|undefined;
|
2017-05-09 16:16:50 -07:00
|
|
|
|
2019-11-27 16:31:00 -08:00
|
|
|
diagnostics: Diagnostic[] = [];
|
2017-05-09 16:16:50 -07:00
|
|
|
|
|
|
|
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(
|
2020-01-12 14:15:50 -08:00
|
|
|
`The template context of '${directive.type.reference.name}' does not define an implicit value.\n` +
|
|
|
|
`If the context type is a base type, consider refining it to a more specific type.`,
|
|
|
|
spanOf(ast.sourceSpan));
|
2017-05-09 16:16:50 -07:00
|
|
|
} else {
|
|
|
|
this.reportError(
|
2020-01-12 14:15:50 -08:00
|
|
|
`The template context of '${directive.type.reference.name}' does not define a member called '${ast.value}'`,
|
2017-05-09 16:16:50 -07:00
|
|
|
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);
|
|
|
|
|
2018-03-10 17:14:58 +00:00
|
|
|
// Find directive that references this template
|
2017-05-09 16:16:50 -07:00
|
|
|
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) {
|
2019-12-16 12:11:39 -08:00
|
|
|
const path = getPathToNodeAtPosition(this.info.htmlAst, ast.sourceSpan.start.offset);
|
2017-05-09 16:16:50 -07:00
|
|
|
const last = path.tail;
|
|
|
|
if (last instanceof Attribute && last.valueSpan) {
|
2019-02-08 22:10:20 +00:00
|
|
|
return last.valueSpan.start.offset;
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
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(
|
2019-11-27 16:31:00 -08:00
|
|
|
{span: offsetSpan(span, this.info.offset), kind: ts.DiagnosticCategory.Error, message});
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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};
|
2019-10-24 10:17:25 -07:00
|
|
|
}
|