refactor(language-service): Replace TypeDiagnostic with ng.Diagnostic (#35085)

This commit cleans up `expression_type.ts` by

1. Removing the unnecessary `TypeDiagnostic` class. It's replaced by
`ng.Diagnostic`.
2. Consolidating `reportError()` and `reportWarning()` to
`reportDiagnostic()`.

This is prep work so that we could make some of the type diagnostics a
suggestion in later PRs.

PR Close #35085
This commit is contained in:
Keen Yee Liau 2020-01-31 12:34:20 -08:00 committed by Miško Hevery
parent 01308e4c7c
commit c8584e5d36
2 changed files with 80 additions and 81 deletions

View File

@ -9,7 +9,7 @@
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'; 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';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AstType, ExpressionDiagnosticsContext, TypeDiagnostic} from './expression_type'; import {AstType} from './expression_type';
import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols'; import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols';
import {Diagnostic} from './types'; import {Diagnostic} from './types';
import {findOutputBinding, getPathToNodeAtPosition} from './utils'; import {findOutputBinding, getPathToNodeAtPosition} from './utils';
@ -30,14 +30,6 @@ export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo):
return visitor.diagnostics; 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[] { function getReferences(info: DiagnosticTemplateInfo): SymbolDeclaration[] {
const result: SymbolDeclaration[] = []; const result: SymbolDeclaration[] = [];
@ -334,15 +326,14 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
return ast.sourceSpan.start.offset; return ast.sourceSpan.start.offset;
} }
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) { private diagnoseExpression(ast: AST, offset: number, event: boolean) {
const scope = this.getExpressionScope(this.path, includeEvent); const scope = this.getExpressionScope(this.path, event);
this.diagnostics.push(...getExpressionDiagnostics(scope, ast, this.info.query, { const analyzer = new AstType(scope, this.info.query, {event});
event: includeEvent for (const {message, span, kind} of analyzer.getDiagnostics(ast)) {
}).map(d => ({ span.start += offset;
span: offsetSpan(d.ast.span, offset + this.info.offset), span.end += offset;
kind: d.kind, this.reportDiagnostic(message as string, span, kind);
message: d.message }
})));
} }
private push(ast: TemplateAst) { this.path.push(ast); } private push(ast: TemplateAst) { this.path.push(ast); }
@ -351,7 +342,9 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
private reportDiagnostic( private reportDiagnostic(
message: string, span: Span, kind: ts.DiagnosticCategory = ts.DiagnosticCategory.Error) { message: string, span: Span, kind: ts.DiagnosticCategory = ts.DiagnosticCategory.Error) {
this.diagnostics.push({span: offsetSpan(span, this.info.offset), kind, message}); span.start += this.info.offset;
span.end += this.info.offset;
this.diagnostics.push({kind, span, message});
} }
} }
@ -366,10 +359,6 @@ function hasTemplateReference(type: CompileTypeMetadata): boolean {
return false; return false;
} }
function offsetSpan(span: Span, amount: number): Span {
return {start: span.start + amount, end: span.end + amount};
}
function spanOf(sourceSpan: ParseSourceSpan): Span { function spanOf(sourceSpan: ParseSourceSpan): Span {
return {start: sourceSpan.start.offset, end: sourceSpan.end.offset}; return {start: sourceSpan.start.offset, end: sourceSpan.end.offset};
} }

View File

@ -10,16 +10,13 @@ import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall,
import * as ts from 'typescript'; import * as ts from 'typescript';
import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols'; import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols';
import * as ng from './types';
export interface ExpressionDiagnosticsContext { event?: boolean; } export interface ExpressionDiagnosticsContext { event?: boolean; }
export class TypeDiagnostic {
constructor(public kind: ts.DiagnosticCategory, public message: string, public ast: AST) {}
}
// AstType calculatetype of the ast given AST element. // AstType calculatetype of the ast given AST element.
export class AstType implements AstVisitor { export class AstType implements AstVisitor {
public diagnostics: TypeDiagnostic[] = []; private readonly diagnostics: ng.Diagnostic[] = [];
constructor( constructor(
private scope: SymbolTable, private query: SymbolQuery, private scope: SymbolTable, private query: SymbolQuery,
@ -27,11 +24,12 @@ export class AstType implements AstVisitor {
getType(ast: AST): Symbol { return ast.visit(this); } getType(ast: AST): Symbol { return ast.visit(this); }
getDiagnostics(ast: AST): TypeDiagnostic[] { getDiagnostics(ast: AST): ng.Diagnostic[] {
this.diagnostics = [];
const type: Symbol = ast.visit(this); const type: Symbol = ast.visit(this);
if (this.context.event && type.callable) { if (this.context.event && type.callable) {
this.reportWarning('Unexpected callable expression. Expected a method call', ast); this.reportDiagnostic(
'Unexpected callable expression. Expected a method call', ast,
ts.DiagnosticCategory.Warning);
} }
return this.diagnostics; return this.diagnostics;
} }
@ -60,7 +58,7 @@ export class AstType implements AstVisitor {
// Nullable allowed. // Nullable allowed.
break; break;
default: default:
this.reportError(`The expression might be null`, ast); this.reportDiagnostic(`The expression might be null`, ast);
break; break;
} }
return this.query.getNonNullableType(type); return this.query.getNonNullableType(type);
@ -104,7 +102,8 @@ export class AstType implements AstVisitor {
errorAst = ast.right; errorAst = ast.right;
break; break;
} }
return this.reportError('Expected a numeric type', errorAst); this.reportDiagnostic('Expected a numeric type', errorAst);
return this.anyType;
} }
case '+': case '+':
switch (operKind) { switch (operKind) {
@ -130,12 +129,15 @@ export class AstType implements AstVisitor {
return this.query.getBuiltinType(BuiltinType.Number); return this.query.getBuiltinType(BuiltinType.Number);
case BuiltinType.Boolean << 8 | BuiltinType.Number: case BuiltinType.Boolean << 8 | BuiltinType.Number:
case BuiltinType.Other << 8 | BuiltinType.Number: case BuiltinType.Other << 8 | BuiltinType.Number:
return this.reportError('Expected a number type', ast.left); this.reportDiagnostic('Expected a number type', ast.left);
return this.anyType;
case BuiltinType.Number << 8 | BuiltinType.Boolean: case BuiltinType.Number << 8 | BuiltinType.Boolean:
case BuiltinType.Number << 8 | BuiltinType.Other: case BuiltinType.Number << 8 | BuiltinType.Other:
return this.reportError('Expected a number type', ast.right); this.reportDiagnostic('Expected a number type', ast.right);
return this.anyType;
default: default:
return this.reportError('Expected operands to be a string or number type', ast); this.reportDiagnostic('Expected operands to be a string or number type', ast);
return this.anyType;
} }
case '>': case '>':
case '<': case '<':
@ -161,7 +163,8 @@ export class AstType implements AstVisitor {
case BuiltinType.Other << 8 | BuiltinType.Other: case BuiltinType.Other << 8 | BuiltinType.Other:
return this.query.getBuiltinType(BuiltinType.Boolean); return this.query.getBuiltinType(BuiltinType.Boolean);
default: default:
return this.reportError('Expected the operants to be of similar type or any', ast); this.reportDiagnostic('Expected the operants to be of similar type or any', ast);
return this.anyType;
} }
case '&&': case '&&':
return rightType; return rightType;
@ -169,23 +172,20 @@ export class AstType implements AstVisitor {
return this.query.getTypeUnion(leftType, rightType); return this.query.getTypeUnion(leftType, rightType);
} }
return this.reportError(`Unrecognized operator ${ast.operation}`, ast); this.reportDiagnostic(`Unrecognized operator ${ast.operation}`, ast);
return this.anyType;
} }
visitChain(ast: Chain) { visitChain(ast: Chain) {
if (this.diagnostics) { // If we are producing diagnostics, visit the children
// If we are producing diagnostics, visit the children visitAstChildren(ast, this);
visitAstChildren(ast, this);
}
// The type of a chain is always undefined. // The type of a chain is always undefined.
return this.query.getBuiltinType(BuiltinType.Undefined); return this.query.getBuiltinType(BuiltinType.Undefined);
} }
visitConditional(ast: Conditional) { visitConditional(ast: Conditional) {
// The type of a conditional is the union of the true and false conditions. // The type of a conditional is the union of the true and false conditions.
if (this.diagnostics) { visitAstChildren(ast, this);
visitAstChildren(ast, this);
}
return this.query.getTypeUnion(this.getType(ast.trueExp), this.getType(ast.falseExp)); return this.query.getTypeUnion(this.getType(ast.trueExp), this.getType(ast.falseExp));
} }
@ -196,11 +196,17 @@ export class AstType implements AstVisitor {
// version. // version.
const args = ast.args.map(arg => this.getType(arg)); const args = ast.args.map(arg => this.getType(arg));
const target = this.getType(ast.target !); const target = this.getType(ast.target !);
if (!target || !target.callable) return this.reportError('Call target is not callable', ast); if (!target || !target.callable) {
this.reportDiagnostic('Call target is not callable', ast);
return this.anyType;
}
const signature = target.selectSignature(args); const signature = target.selectSignature(args);
if (signature) return signature.result; if (signature) {
return signature.result;
}
// TODO: Consider a better error message here. // TODO: Consider a better error message here.
return this.reportError('Unable no compatible signature found for call', ast); this.reportDiagnostic('Unable no compatible signature found for call', ast);
return this.anyType;
} }
visitImplicitReceiver(ast: ImplicitReceiver): Symbol { visitImplicitReceiver(ast: ImplicitReceiver): Symbol {
@ -229,9 +235,7 @@ export class AstType implements AstVisitor {
visitInterpolation(ast: Interpolation): Symbol { visitInterpolation(ast: Interpolation): Symbol {
// If we are producing diagnostics, visit the children. // If we are producing diagnostics, visit the children.
if (this.diagnostics) { visitAstChildren(ast, this);
visitAstChildren(ast, this);
}
return this.undefinedType; return this.undefinedType;
} }
@ -256,9 +260,7 @@ export class AstType implements AstVisitor {
visitLiteralMap(ast: LiteralMap): Symbol { visitLiteralMap(ast: LiteralMap): Symbol {
// If we are producing diagnostics, visit the children // If we are producing diagnostics, visit the children
if (this.diagnostics) { visitAstChildren(ast, this);
visitAstChildren(ast, this);
}
// TODO: Return a composite type. // TODO: Return a composite type.
return this.anyType; return this.anyType;
} }
@ -280,7 +282,8 @@ export class AstType implements AstVisitor {
case 'number': case 'number':
return this.query.getBuiltinType(BuiltinType.Number); return this.query.getBuiltinType(BuiltinType.Number);
default: default:
return this.reportError('Unrecognized primitive', ast); this.reportDiagnostic('Unrecognized primitive', ast);
return this.anyType;
} }
} }
} }
@ -293,19 +296,23 @@ export class AstType implements AstVisitor {
// The type of a pipe node is the return type of the pipe's transform method. The table returned // 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. // by getPipes() is expected to contain symbols with the corresponding transform method type.
const pipe = this.query.getPipes().get(ast.name); const pipe = this.query.getPipes().get(ast.name);
if (!pipe) return this.reportError(`No pipe by the name ${ast.name} found`, ast); if (!pipe) {
this.reportDiagnostic(`No pipe by the name ${ast.name} found`, ast);
return this.anyType;
}
const expType = this.getType(ast.exp); const expType = this.getType(ast.exp);
const signature = const signature =
pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg)))); pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg))));
if (!signature) return this.reportError('Unable to resolve signature for pipe invocation', ast); if (!signature) {
this.reportDiagnostic('Unable to resolve signature for pipe invocation', ast);
return this.anyType;
}
return signature.result; return signature.result;
} }
visitPrefixNot(ast: PrefixNot) { visitPrefixNot(ast: PrefixNot) {
// If we are producing diagnostics, visit the children // If we are producing diagnostics, visit the children
if (this.diagnostics) { visitAstChildren(ast, this);
visitAstChildren(ast, this);
}
// The type of a prefix ! is always boolean. // The type of a prefix ! is always boolean.
return this.query.getBuiltinType(BuiltinType.Boolean); return this.query.getBuiltinType(BuiltinType.Boolean);
} }
@ -362,12 +369,23 @@ export class AstType implements AstVisitor {
// The type of a method is the selected methods result type. // The type of a method is the selected methods result type.
const method = receiverType.members().get(ast.name); const method = receiverType.members().get(ast.name);
if (!method) return this.reportError(`Unknown method '${ast.name}'`, ast); if (!method) {
if (!method.type) return this.reportError(`Could not find a type for '${ast.name}'`, ast); this.reportDiagnostic(`Unknown method '${ast.name}'`, ast);
if (!method.type.callable) return this.reportError(`Member '${ast.name}' is not callable`, ast); return this.anyType;
}
if (!method.type) {
this.reportDiagnostic(`Could not find a type for '${ast.name}'`, ast);
return this.anyType;
}
if (!method.type.callable) {
this.reportDiagnostic(`Member '${ast.name}' is not callable`, ast);
return this.anyType;
}
const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg))); const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg)));
if (!signature) if (!signature) {
return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast); this.reportDiagnostic(`Unable to resolve signature for call of method ${ast.name}`, ast);
return this.anyType;
}
return signature.result; return signature.result;
} }
@ -384,13 +402,14 @@ export class AstType implements AstVisitor {
receiverInfo = receiverInfo =
'The component declaration, template variable declarations, and element references do'; 'The component declaration, template variable declarations, and element references do';
} else if (receiverType.nullable) { } else if (receiverType.nullable) {
return this.reportError(`The expression might be null`, ast.receiver); return this.reportDiagnostic(`The expression might be null`, ast.receiver);
} else { } else {
receiverInfo = `'${receiverInfo}' does`; receiverInfo = `'${receiverInfo}' does`;
} }
return this.reportError( this.reportDiagnostic(
`Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`, `Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`,
ast); ast);
return this.anyType;
} }
if (!member.public) { if (!member.public) {
let receiverInfo = receiverType.name; let receiverInfo = receiverType.name;
@ -399,24 +418,15 @@ export class AstType implements AstVisitor {
} else { } else {
receiverInfo = `'${receiverInfo}'`; receiverInfo = `'${receiverInfo}'`;
} }
this.reportWarning( this.reportDiagnostic(
`Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast); `Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast,
ts.DiagnosticCategory.Warning);
} }
return member.type; return member.type;
} }
private reportError(message: string, ast: AST): Symbol { private reportDiagnostic(message: string, ast: AST, kind = ts.DiagnosticCategory.Error) {
if (this.diagnostics) { this.diagnostics.push({kind, span: ast.span, message});
this.diagnostics.push(new TypeDiagnostic(ts.DiagnosticCategory.Error, message, ast));
}
return this.anyType;
}
private reportWarning(message: string, ast: AST): Symbol {
if (this.diagnostics) {
this.diagnostics.push(new TypeDiagnostic(ts.DiagnosticCategory.Warning, message, ast));
}
return this.anyType;
} }
private isAny(symbol: Symbol): boolean { private isAny(symbol: Symbol): boolean {