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
|
|
|
|
*/
|
|
|
|
|
2020-04-03 20:57:39 -07:00
|
|
|
import {AST, AstPath, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveSummary, CompileTypeMetadata, DirectiveAst, ElementAst, EmbeddedTemplateAst, identifierName, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstPath, templateVisitAll, tokenReference, VariableAst} from '@angular/compiler';
|
2017-05-09 16:16:50 -07:00
|
|
|
|
2020-04-03 20:57:39 -07:00
|
|
|
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
|
2020-01-31 12:34:20 -08:00
|
|
|
import {AstType} from './expression_type';
|
2017-05-09 16:16:50 -07:00
|
|
|
import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols';
|
2020-02-25 08:32:38 -08:00
|
|
|
import * as ng 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[];
|
2020-02-09 12:29:45 -08:00
|
|
|
source: string;
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
|
2020-02-25 08:32:38 -08:00
|
|
|
export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo): ng.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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),
|
2020-04-03 20:57:39 -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) {
|
2020-03-10 12:22:46 +08:00
|
|
|
let symbol = getVariableTypeFromDirectiveContext(variable.value, info.query, current);
|
|
|
|
|
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),
|
|
|
|
]);
|
2020-02-09 12:29:45 -08:00
|
|
|
symbol = refinedVariableType(variable.value, symbolsInScope, info, current);
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
2019-11-06 10:32:45 -08:00
|
|
|
results.push({
|
|
|
|
name: variable.name,
|
|
|
|
kind: 'variable',
|
2020-04-03 20:57:39 -07:00
|
|
|
type: symbol,
|
|
|
|
get definition() {
|
|
|
|
return getDefinitionOf(info, variable);
|
|
|
|
},
|
2019-11-06 10:32:45 -08:00
|
|
|
});
|
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
|
|
|
/**
|
2020-02-19 20:38:21 +01:00
|
|
|
* Resolve the type for the variable in `templateElement` by finding the structural
|
|
|
|
* directive which has the context member. Returns any when not found.
|
|
|
|
* @param value variable value name
|
2019-11-27 11:29:37 -06:00
|
|
|
* @param query type symbol query
|
2020-02-19 20:38:21 +01:00
|
|
|
* @param templateElement
|
2019-11-27 11:29:37 -06:00
|
|
|
*/
|
2020-02-19 20:38:21 +01:00
|
|
|
function getVariableTypeFromDirectiveContext(
|
|
|
|
value: string, query: SymbolQuery, templateElement: EmbeddedTemplateAst): Symbol {
|
|
|
|
for (const {directive} of templateElement.directives) {
|
|
|
|
const context = query.getTemplateContext(directive.type.reference);
|
|
|
|
if (context) {
|
|
|
|
const member = context.get(value);
|
|
|
|
if (member && member.type) {
|
|
|
|
return member.type;
|
|
|
|
}
|
|
|
|
}
|
2019-11-27 11:29:37 -06:00
|
|
|
}
|
2020-03-08 13:42:05 -07:00
|
|
|
|
2020-02-19 20:38:21 +01:00
|
|
|
return query.getBuiltinType(BuiltinType.Any);
|
2019-11-27 11:29:37 -06:00
|
|
|
}
|
|
|
|
|
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
|
2020-02-09 12:29:45 -08:00
|
|
|
* @param info available template information
|
2019-11-06 10:32:45 -08:00
|
|
|
* @param templateElement
|
|
|
|
*/
|
2017-05-09 16:16:50 -07:00
|
|
|
function refinedVariableType(
|
2020-02-09 12:29:45 -08:00
|
|
|
value: string, mergedTable: SymbolTable, info: DiagnosticTemplateInfo,
|
2019-11-27 11:29:37 -06:00
|
|
|
templateElement: EmbeddedTemplateAst): Symbol {
|
2020-02-19 20:38:21 +01:00
|
|
|
if (value === '$implicit') {
|
2020-03-08 13:42:05 -07:00
|
|
|
// Special case: ngFor directive
|
2020-02-19 20:38:21 +01:00
|
|
|
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) {
|
|
|
|
// Check if there is a known type for the ngFor binding.
|
2020-02-09 12:29:45 -08:00
|
|
|
const bindingType =
|
|
|
|
new AstType(mergedTable, info.query, {}, info.source).getType(ngForOfBinding.value);
|
2020-02-19 20:38:21 +01:00
|
|
|
if (bindingType) {
|
2020-02-09 12:29:45 -08:00
|
|
|
const result = info.query.getElementType(bindingType);
|
2020-02-19 20:38:21 +01:00
|
|
|
if (result) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-08 13:42:05 -07:00
|
|
|
if (value === 'ngIf' || value === '$implicit') {
|
2020-02-19 20:38:21 +01:00
|
|
|
const ngIfDirective =
|
|
|
|
templateElement.directives.find(d => identifierName(d.directive.type) === 'NgIf');
|
|
|
|
if (ngIfDirective) {
|
2020-03-08 13:42:05 -07:00
|
|
|
// Special case: ngIf directive. The NgIf structural directive owns a template context with
|
|
|
|
// "$implicit" and "ngIf" members. These properties are typed as generics. Until the language
|
|
|
|
// service uses an Ivy and TypecheckBlock backend, we cannot bind these values to a concrete
|
|
|
|
// type without manual inference. To get the concrete type, look up the type of the "ngIf"
|
|
|
|
// import on the NgIf directive bound to the template.
|
|
|
|
//
|
|
|
|
// See @angular/common/ng_if.ts for more information.
|
2020-02-19 20:38:21 +01:00
|
|
|
const ngIfBinding = ngIfDirective.inputs.find(i => i.directiveName === 'ngIf');
|
|
|
|
if (ngIfBinding) {
|
2020-03-08 13:42:05 -07:00
|
|
|
// Check if there is a known type bound to the ngIf input.
|
2020-02-09 12:29:45 -08:00
|
|
|
const bindingType =
|
|
|
|
new AstType(mergedTable, info.query, {}, info.source).getType(ngIfBinding.value);
|
2020-02-19 20:38:21 +01:00
|
|
|
if (bindingType) {
|
|
|
|
return bindingType;
|
|
|
|
}
|
2019-10-05 15:18:05 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-19 10:07:54 -07:00
|
|
|
// We can't do better, return any
|
2020-02-09 12:29:45 -08:00
|
|
|
return info.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
|
|
|
|
2020-02-25 08:32:38 -08:00
|
|
|
diagnostics: ng.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) {
|
2020-04-03 20:57:39 -07:00
|
|
|
const context = this.info.query.getTemplateContext(directive.type.reference)!;
|
2017-05-09 16:16:50 -07:00
|
|
|
if (context && !context.has(ast.value)) {
|
2020-01-29 10:17:36 -08:00
|
|
|
const missingMember =
|
|
|
|
ast.value === '$implicit' ? 'an implicit value' : `a member called '${ast.value}'`;
|
2020-02-25 08:32:38 -08:00
|
|
|
|
|
|
|
const span = this.absSpan(spanOf(ast.sourceSpan));
|
|
|
|
this.diagnostics.push(createDiagnostic(
|
|
|
|
span, Diagnostic.template_context_missing_member, directive.type.reference.name,
|
|
|
|
missingMember));
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 =
|
2020-04-03 20:57:39 -07:00
|
|
|
ast.directives.map(d => d.directive).find(d => hasTemplateReference(d.type))!;
|
2017-05-09 16:16:50 -07:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2020-02-09 12:29:45 -08:00
|
|
|
private diagnoseExpression(ast: AST, offset: number, inEvent: boolean) {
|
|
|
|
const scope = this.getExpressionScope(this.path, inEvent);
|
|
|
|
const analyzer = new AstType(scope, this.info.query, {inEvent}, this.info.source);
|
2020-02-25 08:32:38 -08:00
|
|
|
for (const diagnostic of analyzer.getDiagnostics(ast)) {
|
|
|
|
diagnostic.span = this.absSpan(diagnostic.span, offset);
|
|
|
|
this.diagnostics.push(diagnostic);
|
2020-01-31 12:34:20 -08:00
|
|
|
}
|
2017-05-09 16:16:50 -07:00
|
|
|
}
|
|
|
|
|
2020-04-03 20:57:39 -07:00
|
|
|
private push(ast: TemplateAst) {
|
|
|
|
this.path.push(ast);
|
|
|
|
}
|
2017-05-09 16:16:50 -07:00
|
|
|
|
2020-04-03 20:57:39 -07:00
|
|
|
private pop() {
|
|
|
|
this.path.pop();
|
|
|
|
}
|
2017-05-09 16:16:50 -07:00
|
|
|
|
2020-02-25 08:32:38 -08:00
|
|
|
private absSpan(span: Span, additionalOffset: number = 0): Span {
|
|
|
|
return {
|
|
|
|
start: span.start + this.info.offset + additionalOffset,
|
|
|
|
end: span.end + this.info.offset + additionalOffset,
|
|
|
|
};
|
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 &&
|
2020-04-03 20:57:39 -07:00
|
|
|
identifierName(diDep.token!.identifier!) == 'TemplateRef')
|
2017-05-09 16:16:50 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function spanOf(sourceSpan: ParseSourceSpan): Span {
|
|
|
|
return {start: sourceSpan.start.offset, end: sourceSpan.end.offset};
|
2019-10-24 10:17:25 -07:00
|
|
|
}
|