angular-docs-cn/packages/language-service/src/expression_diagnostics.ts
Keen Yee Liau 1140bbc25c refactor(language-service): reformat using clang-format (#36426)
clang-format was recently updated and any PRs that touch files in the
language service will have to reformat all the files.

Instead of changing the formatting in those PRs, this PR formats all
files in language-service package once and for all.

PR Close #36426
2020-04-06 09:29:42 -07:00

388 lines
13 KiB
TypeScript

/**
* @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, identifierName, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstPath, templateVisitAll, tokenReference, VariableAst} from '@angular/compiler';
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
import {AstType} from './expression_type';
import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols';
import * as ng from './types';
import {findOutputBinding, getPathToNodeAtPosition} from './utils';
export interface DiagnosticTemplateInfo {
fileName?: string;
offset: number;
query: SymbolQuery;
members: SymbolTable;
htmlAst: Node[];
templateAst: TemplateAst[];
source: string;
}
export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo): ng.Diagnostic[] {
const visitor = new ExpressionDiagnosticsVisitor(
info, (path: TemplateAstPath) => getExpressionScope(info, path));
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),
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 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;
}
for (const variable of current.variables) {
let symbol = getVariableTypeFromDirectiveContext(variable.value, info.query, current);
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(variable.value, symbolsInScope, info, current);
}
results.push({
name: variable.name,
kind: 'variable',
type: symbol,
get definition() {
return getDefinitionOf(info, variable);
},
});
}
}
return results;
}
/**
* 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
* @param query type symbol query
* @param templateElement
*/
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;
}
}
}
return query.getBuiltinType(BuiltinType.Any);
}
/**
* 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 value variable value name
* @param mergedTable symbol table for all variables in scope
* @param info available template information
* @param templateElement
*/
function refinedVariableType(
value: string, mergedTable: SymbolTable, info: DiagnosticTemplateInfo,
templateElement: EmbeddedTemplateAst): Symbol {
if (value === '$implicit') {
// Special case: 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) {
// Check if there is a known type for the ngFor binding.
const bindingType =
new AstType(mergedTable, info.query, {}, info.source).getType(ngForOfBinding.value);
if (bindingType) {
const result = info.query.getElementType(bindingType);
if (result) {
return result;
}
}
}
}
}
if (value === 'ngIf' || value === '$implicit') {
const ngIfDirective =
templateElement.directives.find(d => identifierName(d.directive.type) === 'NgIf');
if (ngIfDirective) {
// 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.
const ngIfBinding = ngIfDirective.inputs.find(i => i.directiveName === 'ngIf');
if (ngIfBinding) {
// Check if there is a known type bound to the ngIf input.
const bindingType =
new AstType(mergedTable, info.query, {}, info.source).getType(ngIfBinding.value);
if (bindingType) {
return bindingType;
}
}
}
}
// We can't do better, return any
return info.query.getBuiltinType(BuiltinType.Any);
}
function getEventDeclaration(
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration|undefined {
const event = path.tail;
if (!(event instanceof BoundEventAst)) {
// No event available in this context.
return;
}
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};
}
/**
* 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.
*/
export function getExpressionScope(
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolTable {
let result = info.members;
const references = getReferences(info);
const variables = getVarDeclarations(info, path);
const event = getEventDeclaration(info, path);
if (references.length || variables.length || event) {
const referenceTable = info.query.createSymbolTable(references);
const variableTable = info.query.createSymbolTable(variables);
const eventsTable = info.query.createSymbolTable(event ? [event] : []);
result = info.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]);
}
return result;
}
class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
private path: TemplateAstPath;
private directiveSummary: CompileDirectiveSummary|undefined;
diagnostics: ng.Diagnostic[] = [];
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)) {
const missingMember =
ast.value === '$implicit' ? 'an implicit value' : `a member called '${ast.value}'`;
const span = this.absSpan(spanOf(ast.sourceSpan));
this.diagnostics.push(createDiagnostic(
span, Diagnostic.template_context_missing_member, directive.type.reference.name,
missingMember));
}
}
}
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 = getPathToNodeAtPosition(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, inEvent: boolean) {
const scope = this.getExpressionScope(this.path, inEvent);
const analyzer = new AstType(scope, this.info.query, {inEvent}, this.info.source);
for (const diagnostic of analyzer.getDiagnostics(ast)) {
diagnostic.span = this.absSpan(diagnostic.span, offset);
this.diagnostics.push(diagnostic);
}
}
private push(ast: TemplateAst) {
this.path.push(ast);
}
private pop() {
this.path.pop();
}
private absSpan(span: Span, additionalOffset: number = 0): Span {
return {
start: span.start + this.info.offset + additionalOffset,
end: span.end + this.info.offset + additionalOffset,
};
}
}
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 spanOf(sourceSpan: ParseSourceSpan): Span {
return {start: sourceSpan.start.offset, end: sourceSpan.end.offset};
}