feat(language-service): modularize error messages (#35678)
This commit performs a modularization of the Language Service's existing diagnostic messages. Such a modularization has two primary advantages: - Centralization and decoupling of error messages from the code that generates them makes it easy to add/delete/edit diagnostic messages, and allows for independent iteration of diagnostic messages and diagnostic generation. - Prepares for additional features like annotating the locations where a diagnostic is generated and enabling the configuration of which diagnostics should be reported by the language service. Although it would be preferable to place the diagnostics registry in an independent JSON file, for ease of typing diagnostic types as an enum variant of 'ts.DiagnosticCategory', the registry is stored as an object. Part of #32663. PR Close #35678
This commit is contained in:
parent
8eb4a9d395
commit
47a1811e0b
|
@ -0,0 +1,161 @@
|
||||||
|
/**
|
||||||
|
* @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 * as ts from 'typescript';
|
||||||
|
import * as ng from './types';
|
||||||
|
|
||||||
|
export interface DiagnosticMessage {
|
||||||
|
message: string;
|
||||||
|
kind: keyof typeof ts.DiagnosticCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiagnosticName = 'directive_not_in_module' | 'missing_template_and_templateurl' |
|
||||||
|
'both_template_and_templateurl' | 'invalid_templateurl' | 'template_context_missing_member' |
|
||||||
|
'callable_expression_expected_method_call' | 'call_target_not_callable' |
|
||||||
|
'expression_might_be_null' | 'expected_a_number_type' | 'expected_a_string_or_number_type' |
|
||||||
|
'expected_operands_of_similar_type_or_any' | 'unrecognized_operator' |
|
||||||
|
'unrecognized_primitive' | 'no_pipe_found' | 'unable_to_resolve_compatible_call_signature' |
|
||||||
|
'unable_to_resolve_signature' | 'could_not_resolve_type' | 'identifier_not_callable' |
|
||||||
|
'identifier_possibly_undefined' | 'identifier_not_defined_in_app_context' |
|
||||||
|
'identifier_not_defined_on_receiver' | 'identifier_is_private';
|
||||||
|
|
||||||
|
export const Diagnostic: Record<DiagnosticName, DiagnosticMessage> = {
|
||||||
|
directive_not_in_module: {
|
||||||
|
message:
|
||||||
|
`%1 '%2' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.`,
|
||||||
|
kind: 'Suggestion',
|
||||||
|
},
|
||||||
|
|
||||||
|
missing_template_and_templateurl: {
|
||||||
|
message: `Component '%1' must have a template or templateUrl`,
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
both_template_and_templateurl: {
|
||||||
|
message: `Component '%1' must not have both template and templateUrl`,
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
invalid_templateurl: {
|
||||||
|
message: `URL does not point to a valid file`,
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
template_context_missing_member: {
|
||||||
|
message: `The template context of '%1' does not define %2.\n` +
|
||||||
|
`If the context type is a base type or 'any', consider refining it to a more specific type.`,
|
||||||
|
kind: 'Suggestion',
|
||||||
|
},
|
||||||
|
|
||||||
|
callable_expression_expected_method_call: {
|
||||||
|
message: 'Unexpected callable expression. Expected a method call',
|
||||||
|
kind: 'Warning',
|
||||||
|
},
|
||||||
|
|
||||||
|
call_target_not_callable: {
|
||||||
|
message: 'Call target is not callable',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
expression_might_be_null: {
|
||||||
|
message: 'The expression might be null',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
expected_a_number_type: {
|
||||||
|
message: 'Expected a number type',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
expected_a_string_or_number_type: {
|
||||||
|
message: 'Expected operands to be a string or number type',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
expected_operands_of_similar_type_or_any: {
|
||||||
|
message: 'Expected operands to be of similar type or any',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
unrecognized_operator: {
|
||||||
|
message: 'Unrecognized operator %1',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
unrecognized_primitive: {
|
||||||
|
message: 'Unrecognized primitive %1',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
no_pipe_found: {
|
||||||
|
message: 'No pipe of name %1 found',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: Consider a better error message here.
|
||||||
|
unable_to_resolve_compatible_call_signature: {
|
||||||
|
message: 'Unable to resolve compatible call signature',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
unable_to_resolve_signature: {
|
||||||
|
message: 'Unable to resolve signature for call of %1',
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
could_not_resolve_type: {
|
||||||
|
message: `Could not resolve the type of '%1'`,
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
identifier_not_callable: {
|
||||||
|
message: `'%1' is not callable`,
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
identifier_possibly_undefined: {
|
||||||
|
message:
|
||||||
|
`'%1' is possibly undefined. Consider using the safe navigation operator (%2) or non-null assertion operator (%3).`,
|
||||||
|
kind: 'Suggestion',
|
||||||
|
},
|
||||||
|
|
||||||
|
identifier_not_defined_in_app_context: {
|
||||||
|
message:
|
||||||
|
`Identifier '%1' is not defined. The component declaration, template variable declarations, and element references do not contain such a member`,
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
identifier_not_defined_on_receiver: {
|
||||||
|
message: `Identifier '%1' is not defined. '%2' does not contain such a member`,
|
||||||
|
kind: 'Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
identifier_is_private: {
|
||||||
|
message: `Identifier '%1' refers to a private member of %2`,
|
||||||
|
kind: 'Warning',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a language service diagnostic.
|
||||||
|
* @param span location the diagnostic for
|
||||||
|
* @param dm diagnostic message
|
||||||
|
* @param formatArgs run-time arguments to format the diagnostic message with (see the messages in
|
||||||
|
* the `Diagnostic` object for an example).
|
||||||
|
* @returns a created diagnostic
|
||||||
|
*/
|
||||||
|
export function createDiagnostic(
|
||||||
|
span: ng.Span, dm: DiagnosticMessage, ...formatArgs: string[]): ng.Diagnostic {
|
||||||
|
// Formats "%1 %2" with formatArgs ['a', 'b'] as "a b"
|
||||||
|
const formattedMessage =
|
||||||
|
dm.message.replace(/%(\d+)/g, (_, index: string) => formatArgs[+index - 1]);
|
||||||
|
return {
|
||||||
|
kind: ts.DiagnosticCategory[dm.kind],
|
||||||
|
message: formattedMessage, span,
|
||||||
|
};
|
||||||
|
}
|
|
@ -11,12 +11,14 @@ import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {AstResult} from './common';
|
import {AstResult} from './common';
|
||||||
|
import {Diagnostic, createDiagnostic} from './diagnostic_messages';
|
||||||
import {getTemplateExpressionDiagnostics} from './expression_diagnostics';
|
import {getTemplateExpressionDiagnostics} from './expression_diagnostics';
|
||||||
import * as ng from './types';
|
import * as ng from './types';
|
||||||
import {TypeScriptServiceHost} from './typescript_host';
|
import {TypeScriptServiceHost} from './typescript_host';
|
||||||
import {findPropertyValueOfType, findTightestNode, offsetSpan, spanOf} from './utils';
|
import {findPropertyValueOfType, findTightestNode, offsetSpan, spanOf} from './utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return diagnostic information for the parsed AST of the template.
|
* Return diagnostic information for the parsed AST of the template.
|
||||||
* @param ast contains HTML and template AST
|
* @param ast contains HTML and template AST
|
||||||
|
@ -41,18 +43,6 @@ export function getTemplateDiagnostics(ast: AstResult): ng.Diagnostic[] {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an error message that indicates a directive is not part of any
|
|
||||||
* NgModule.
|
|
||||||
* @param name class name
|
|
||||||
* @param isComponent true if directive is an Angular Component
|
|
||||||
*/
|
|
||||||
function missingDirective(name: string, isComponent: boolean) {
|
|
||||||
const type = isComponent ? 'Component' : 'Directive';
|
|
||||||
return `${type} '${name}' is not included in a module and will not be ` +
|
|
||||||
'available inside a template. Consider adding it to a NgModule declaration.';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a variety diagnostics on directive declarations.
|
* Performs a variety diagnostics on directive declarations.
|
||||||
*
|
*
|
||||||
|
@ -96,28 +86,22 @@ export function getDeclarationDiagnostics(
|
||||||
span: error.span,
|
span: error.span,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!modules.ngModuleByPipeOrDirective.has(declaration.type)) {
|
||||||
|
results.push(createDiagnostic(
|
||||||
|
declarationSpan, Diagnostic.directive_not_in_module,
|
||||||
|
metadata.isComponent ? 'Component' : 'Directive', type.name));
|
||||||
|
}
|
||||||
|
|
||||||
if (metadata.isComponent) {
|
if (metadata.isComponent) {
|
||||||
if (!modules.ngModuleByPipeOrDirective.has(declaration.type)) {
|
|
||||||
results.push({
|
|
||||||
kind: ts.DiagnosticCategory.Suggestion,
|
|
||||||
message: missingDirective(type.name, metadata.isComponent),
|
|
||||||
span: declarationSpan,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const {template, templateUrl, styleUrls} = metadata.template !;
|
const {template, templateUrl, styleUrls} = metadata.template !;
|
||||||
if (template === null && !templateUrl) {
|
if (template === null && !templateUrl) {
|
||||||
results.push({
|
results.push(createDiagnostic(
|
||||||
kind: ts.DiagnosticCategory.Error,
|
declarationSpan, Diagnostic.missing_template_and_templateurl, type.name));
|
||||||
message: `Component '${type.name}' must have a template or templateUrl`,
|
|
||||||
span: declarationSpan,
|
|
||||||
});
|
|
||||||
} else if (templateUrl) {
|
} else if (templateUrl) {
|
||||||
if (template) {
|
if (template) {
|
||||||
results.push({
|
results.push(createDiagnostic(
|
||||||
kind: ts.DiagnosticCategory.Error,
|
declarationSpan, Diagnostic.both_template_and_templateurl, type.name));
|
||||||
message: `Component '${type.name}' must not have both template and templateUrl`,
|
|
||||||
span: declarationSpan,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find templateUrl value from the directive call expression, which is the parent of the
|
// Find templateUrl value from the directive call expression, which is the parent of the
|
||||||
|
@ -147,12 +131,6 @@ export function getDeclarationDiagnostics(
|
||||||
|
|
||||||
results.push(...validateUrls(styleUrlsNode.elements, host.tsLsHost));
|
results.push(...validateUrls(styleUrlsNode.elements, host.tsLsHost));
|
||||||
}
|
}
|
||||||
} else if (!directives.has(declaration.type)) {
|
|
||||||
results.push({
|
|
||||||
kind: ts.DiagnosticCategory.Suggestion,
|
|
||||||
message: missingDirective(type.name, metadata.isComponent),
|
|
||||||
span: declarationSpan,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,12 +166,9 @@ function validateUrls(
|
||||||
const url = path.join(path.dirname(curPath), urlNode.text);
|
const url = path.join(path.dirname(curPath), urlNode.text);
|
||||||
if (tsLsHost.fileExists(url)) continue;
|
if (tsLsHost.fileExists(url)) continue;
|
||||||
|
|
||||||
allErrors.push({
|
// Exclude opening and closing quotes in the url span.
|
||||||
kind: ts.DiagnosticCategory.Error,
|
const urlSpan = {start: urlNode.getStart() + 1, end: urlNode.end - 1};
|
||||||
message: `URL does not point to a valid file`,
|
allErrors.push(createDiagnostic(urlSpan, Diagnostic.invalid_templateurl));
|
||||||
// Exclude opening and closing quotes in the url span.
|
|
||||||
span: {start: urlNode.getStart() + 1, end: urlNode.end - 1},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return allErrors;
|
return allErrors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 {Diagnostic, createDiagnostic} from './diagnostic_messages';
|
||||||
import {AstType} 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 * as ng from './types';
|
||||||
import {findOutputBinding, getPathToNodeAtPosition} from './utils';
|
import {findOutputBinding, getPathToNodeAtPosition} from './utils';
|
||||||
|
|
||||||
export interface DiagnosticTemplateInfo {
|
export interface DiagnosticTemplateInfo {
|
||||||
|
@ -23,7 +23,7 @@ export interface DiagnosticTemplateInfo {
|
||||||
templateAst: TemplateAst[];
|
templateAst: TemplateAst[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo): Diagnostic[] {
|
export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo): ng.Diagnostic[] {
|
||||||
const visitor = new ExpressionDiagnosticsVisitor(
|
const visitor = new ExpressionDiagnosticsVisitor(
|
||||||
info, (path: TemplateAstPath) => getExpressionScope(info, path));
|
info, (path: TemplateAstPath) => getExpressionScope(info, path));
|
||||||
templateVisitAll(visitor, info.templateAst);
|
templateVisitAll(visitor, info.templateAst);
|
||||||
|
@ -244,7 +244,7 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
|
||||||
private path: TemplateAstPath;
|
private path: TemplateAstPath;
|
||||||
private directiveSummary: CompileDirectiveSummary|undefined;
|
private directiveSummary: CompileDirectiveSummary|undefined;
|
||||||
|
|
||||||
diagnostics: Diagnostic[] = [];
|
diagnostics: ng.Diagnostic[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private info: DiagnosticTemplateInfo,
|
private info: DiagnosticTemplateInfo,
|
||||||
|
@ -291,10 +291,11 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
|
||||||
if (context && !context.has(ast.value)) {
|
if (context && !context.has(ast.value)) {
|
||||||
const missingMember =
|
const missingMember =
|
||||||
ast.value === '$implicit' ? 'an implicit value' : `a member called '${ast.value}'`;
|
ast.value === '$implicit' ? 'an implicit value' : `a member called '${ast.value}'`;
|
||||||
this.reportDiagnostic(
|
|
||||||
`The template context of '${directive.type.reference.name}' does not define ${missingMember}.\n` +
|
const span = this.absSpan(spanOf(ast.sourceSpan));
|
||||||
`If the context type is a base type or 'any', consider refining it to a more specific type.`,
|
this.diagnostics.push(createDiagnostic(
|
||||||
spanOf(ast.sourceSpan), ts.DiagnosticCategory.Suggestion);
|
span, Diagnostic.template_context_missing_member, directive.type.reference.name,
|
||||||
|
missingMember));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -334,10 +335,9 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
|
||||||
private diagnoseExpression(ast: AST, offset: number, event: boolean) {
|
private diagnoseExpression(ast: AST, offset: number, event: boolean) {
|
||||||
const scope = this.getExpressionScope(this.path, event);
|
const scope = this.getExpressionScope(this.path, event);
|
||||||
const analyzer = new AstType(scope, this.info.query, {event});
|
const analyzer = new AstType(scope, this.info.query, {event});
|
||||||
for (const {message, span, kind} of analyzer.getDiagnostics(ast)) {
|
for (const diagnostic of analyzer.getDiagnostics(ast)) {
|
||||||
span.start += offset;
|
diagnostic.span = this.absSpan(diagnostic.span, offset);
|
||||||
span.end += offset;
|
this.diagnostics.push(diagnostic);
|
||||||
this.reportDiagnostic(message as string, span, kind);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,11 +345,11 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
|
||||||
|
|
||||||
private pop() { this.path.pop(); }
|
private pop() { this.path.pop(); }
|
||||||
|
|
||||||
private reportDiagnostic(
|
private absSpan(span: Span, additionalOffset: number = 0): Span {
|
||||||
message: string, span: Span, kind: ts.DiagnosticCategory = ts.DiagnosticCategory.Error) {
|
return {
|
||||||
span.start += this.info.offset;
|
start: span.start + this.info.offset + additionalOffset,
|
||||||
span.end += this.info.offset;
|
end: span.end + this.info.offset + additionalOffset,
|
||||||
this.diagnostics.push({kind, span, message});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
|
||||||
|
|
||||||
|
import {Diagnostic, createDiagnostic} from './diagnostic_messages';
|
||||||
import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols';
|
import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols';
|
||||||
import * as ng from './types';
|
import * as ng from './types';
|
||||||
|
|
||||||
|
@ -27,9 +27,8 @@ export class AstType implements AstVisitor {
|
||||||
getDiagnostics(ast: AST): ng.Diagnostic[] {
|
getDiagnostics(ast: AST): ng.Diagnostic[] {
|
||||||
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.reportDiagnostic(
|
this.diagnostics.push(
|
||||||
'Unexpected callable expression. Expected a method call', ast,
|
createDiagnostic(ast.span, Diagnostic.callable_expression_expected_method_call));
|
||||||
ts.DiagnosticCategory.Warning);
|
|
||||||
}
|
}
|
||||||
return this.diagnostics;
|
return this.diagnostics;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +57,7 @@ export class AstType implements AstVisitor {
|
||||||
// Nullable allowed.
|
// Nullable allowed.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.reportDiagnostic(`The expression might be null`, ast);
|
this.diagnostics.push(createDiagnostic(ast.span, Diagnostic.expression_might_be_null));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return this.query.getNonNullableType(type);
|
return this.query.getNonNullableType(type);
|
||||||
|
@ -102,7 +101,8 @@ export class AstType implements AstVisitor {
|
||||||
errorAst = ast.right;
|
errorAst = ast.right;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.reportDiagnostic('Expected a numeric type', errorAst);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(errorAst.span, Diagnostic.expected_a_number_type));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
case '+':
|
case '+':
|
||||||
|
@ -129,14 +129,17 @@ 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:
|
||||||
this.reportDiagnostic('Expected a number type', ast.left);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.left.span, Diagnostic.expected_a_number_type));
|
||||||
return this.anyType;
|
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:
|
||||||
this.reportDiagnostic('Expected a number type', ast.right);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.right.span, Diagnostic.expected_a_number_type));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
default:
|
default:
|
||||||
this.reportDiagnostic('Expected operands to be a string or number type', ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.expected_a_string_or_number_type));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
case '>':
|
case '>':
|
||||||
|
@ -163,7 +166,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:
|
||||||
this.reportDiagnostic('Expected the operants to be of similar type or any', ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.expected_operands_of_similar_type_or_any));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
case '&&':
|
case '&&':
|
||||||
|
@ -172,7 +176,8 @@ export class AstType implements AstVisitor {
|
||||||
return this.query.getTypeUnion(leftType, rightType);
|
return this.query.getTypeUnion(leftType, rightType);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reportDiagnostic(`Unrecognized operator ${ast.operation}`, ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.unrecognized_operator, ast.operation));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +206,7 @@ export class AstType implements AstVisitor {
|
||||||
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) {
|
if (!target || !target.callable) {
|
||||||
this.reportDiagnostic('Call target is not callable', ast);
|
this.diagnostics.push(createDiagnostic(ast.span, Diagnostic.call_target_not_callable));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
const signature = target.selectSignature(args);
|
const signature = target.selectSignature(args);
|
||||||
|
@ -209,7 +214,8 @@ export class AstType implements AstVisitor {
|
||||||
return signature.result;
|
return signature.result;
|
||||||
}
|
}
|
||||||
// TODO: Consider a better error message here.
|
// TODO: Consider a better error message here.
|
||||||
this.reportDiagnostic('Unable no compatible signature found for call', ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.unable_to_resolve_compatible_call_signature));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +296,8 @@ export class AstType implements AstVisitor {
|
||||||
case 'number':
|
case 'number':
|
||||||
return this.query.getBuiltinType(BuiltinType.Number);
|
return this.query.getBuiltinType(BuiltinType.Number);
|
||||||
default:
|
default:
|
||||||
this.reportDiagnostic('Unrecognized primitive', ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.unrecognized_primitive, typeof ast.value));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -305,14 +312,15 @@ export class AstType implements AstVisitor {
|
||||||
// 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) {
|
if (!pipe) {
|
||||||
this.reportDiagnostic(`No pipe by the name ${ast.name} found`, ast);
|
this.diagnostics.push(createDiagnostic(ast.span, Diagnostic.no_pipe_found, ast.name));
|
||||||
return this.anyType;
|
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) {
|
if (!signature) {
|
||||||
this.reportDiagnostic('Unable to resolve signature for pipe invocation', ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.unable_to_resolve_signature, ast.name));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
return signature.result;
|
return signature.result;
|
||||||
|
@ -376,19 +384,22 @@ export class AstType implements AstVisitor {
|
||||||
}
|
}
|
||||||
const methodType = this.resolvePropertyRead(receiverType, ast);
|
const methodType = this.resolvePropertyRead(receiverType, ast);
|
||||||
if (!methodType) {
|
if (!methodType) {
|
||||||
this.reportDiagnostic(`Could not find a type for '${ast.name}'`, ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.could_not_resolve_type, ast.name));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
if (this.isAny(methodType)) {
|
if (this.isAny(methodType)) {
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
if (!methodType.callable) {
|
if (!methodType.callable) {
|
||||||
this.reportDiagnostic(`Member '${ast.name}' is not callable`, ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.identifier_not_callable, ast.name));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
const signature = methodType.selectSignature(ast.args.map(arg => this.getType(arg)));
|
const signature = methodType.selectSignature(ast.args.map(arg => this.getType(arg)));
|
||||||
if (!signature) {
|
if (!signature) {
|
||||||
this.reportDiagnostic(`Unable to resolve signature for call of method ${ast.name}`, ast);
|
this.diagnostics.push(
|
||||||
|
createDiagnostic(ast.span, Diagnostic.unable_to_resolve_signature, ast.name));
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
return signature.result;
|
return signature.result;
|
||||||
|
@ -402,37 +413,28 @@ export class AstType implements AstVisitor {
|
||||||
const member = receiverType.members().get(ast.name);
|
const member = receiverType.members().get(ast.name);
|
||||||
if (!member) {
|
if (!member) {
|
||||||
if (receiverType.name === '$implicit') {
|
if (receiverType.name === '$implicit') {
|
||||||
this.reportDiagnostic(
|
this.diagnostics.push(
|
||||||
`Identifier '${ast.name}' is not defined. ` +
|
createDiagnostic(ast.span, Diagnostic.identifier_not_defined_in_app_context, ast.name));
|
||||||
`The component declaration, template variable declarations, and element references do not contain such a member`,
|
|
||||||
ast);
|
|
||||||
} else if (receiverType.nullable && ast.receiver instanceof PropertyRead) {
|
} else if (receiverType.nullable && ast.receiver instanceof PropertyRead) {
|
||||||
const receiver = ast.receiver.name;
|
const receiver = ast.receiver.name;
|
||||||
this.reportDiagnostic(
|
this.diagnostics.push(createDiagnostic(
|
||||||
`'${receiver}' is possibly undefined. Consider using the safe navigation operator (${receiver}?.${ast.name}) ` +
|
ast.span, Diagnostic.identifier_possibly_undefined, receiver,
|
||||||
`or non-null assertion operator (${receiver}!.${ast.name}).`,
|
`${receiver}?.${ast.name}`, `${receiver}!.${ast.name}`));
|
||||||
ast, ts.DiagnosticCategory.Suggestion);
|
|
||||||
} else {
|
} else {
|
||||||
this.reportDiagnostic(
|
this.diagnostics.push(createDiagnostic(
|
||||||
`Identifier '${ast.name}' is not defined. '${receiverType.name}' does not contain such a member`,
|
ast.span, Diagnostic.identifier_not_defined_on_receiver, ast.name, receiverType.name));
|
||||||
ast);
|
|
||||||
}
|
}
|
||||||
return this.anyType;
|
return this.anyType;
|
||||||
}
|
}
|
||||||
if (!member.public) {
|
if (!member.public) {
|
||||||
this.reportDiagnostic(
|
const container =
|
||||||
`Identifier '${ast.name}' refers to a private member of ${receiverType.name === '$implicit' ? 'the component' : `
|
receiverType.name === '$implicit' ? 'the component' : `'${receiverType.name}'`;
|
||||||
'${receiverType.name}'
|
this.diagnostics.push(
|
||||||
`}`,
|
createDiagnostic(ast.span, Diagnostic.identifier_is_private, ast.name, container));
|
||||||
ast, ts.DiagnosticCategory.Warning);
|
|
||||||
}
|
}
|
||||||
return member.type;
|
return member.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
private reportDiagnostic(message: string, ast: AST, kind = ts.DiagnosticCategory.Error) {
|
|
||||||
this.diagnostics.push({kind, span: ast.span, message});
|
|
||||||
}
|
|
||||||
|
|
||||||
private isAny(symbol: Symbol): boolean {
|
private isAny(symbol: Symbol): boolean {
|
||||||
return !symbol || this.query.getTypeKind(symbol) === BuiltinType.Any ||
|
return !symbol || this.query.getTypeKind(symbol) === BuiltinType.Any ||
|
||||||
(!!symbol.type && this.isAny(symbol.type));
|
(!!symbol.type && this.isAny(symbol.type));
|
||||||
|
|
|
@ -42,6 +42,7 @@ ts_library(
|
||||||
name = "infra_test_lib",
|
name = "infra_test_lib",
|
||||||
testonly = True,
|
testonly = True,
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"diagnostic_messages_spec.ts",
|
||||||
"global_symbols_spec.ts",
|
"global_symbols_spec.ts",
|
||||||
"html_info_spec.ts",
|
"html_info_spec.ts",
|
||||||
"language_service_spec.ts",
|
"language_service_spec.ts",
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* @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 * as ts from 'typescript';
|
||||||
|
import {DiagnosticMessage, createDiagnostic} from '../src/diagnostic_messages';
|
||||||
|
|
||||||
|
describe('create diagnostic', () => {
|
||||||
|
it('should format and create diagnostics correctly', () => {
|
||||||
|
const diagnosticMessage: DiagnosticMessage = {
|
||||||
|
message: 'Check that %1 contains %2',
|
||||||
|
kind: 'Error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const diagnostic =
|
||||||
|
createDiagnostic({start: 0, end: 1}, diagnosticMessage, 'testCls', 'testMethod');
|
||||||
|
|
||||||
|
expect(diagnostic).toEqual({
|
||||||
|
kind: ts.DiagnosticCategory.Error,
|
||||||
|
message: 'Check that testCls contains testMethod',
|
||||||
|
span: {start: 0, end: 1},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -85,7 +85,7 @@ describe('diagnostics', () => {
|
||||||
mockHost.override(TEST_TEMPLATE, template);
|
mockHost.override(TEST_TEMPLATE, template);
|
||||||
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||||
expect(diags.length).toBe(1);
|
expect(diags.length).toBe(1);
|
||||||
expect(diags[0].messageText).toBe('Unable to resolve signature for call of method $any');
|
expect(diags[0].messageText).toBe('Unable to resolve signature for call of $any');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ describe('diagnostics', () => {
|
||||||
`);
|
`);
|
||||||
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||||
expect(diags.length).toBe(1);
|
expect(diags.length).toBe(1);
|
||||||
expect(diags[0].messageText).toBe(`Expected the operants to be of similar type or any`);
|
expect(diags[0].messageText).toBe(`Expected operands to be of similar type or any`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not report errors for matching exported type', () => {
|
it('should not report errors for matching exported type', () => {
|
||||||
|
@ -228,7 +228,7 @@ describe('diagnostics', () => {
|
||||||
|
|
||||||
it('should report numeric operator errors', () => {
|
it('should report numeric operator errors', () => {
|
||||||
const diags = ngLS.getSemanticDiagnostics(EXPRESSION_CASES).map(d => d.messageText);
|
const diags = ngLS.getSemanticDiagnostics(EXPRESSION_CASES).map(d => d.messageText);
|
||||||
expect(diags).toContain('Expected a numeric type');
|
expect(diags).toContain('Expected a number type');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ describe('expression diagnostics', () => {
|
||||||
it('should reject a misspelled field of a method result',
|
it('should reject a misspelled field of a method result',
|
||||||
() => reject('{{getPerson().nume.first}}', 'Identifier \'nume\' is not defined'));
|
() => reject('{{getPerson().nume.first}}', 'Identifier \'nume\' is not defined'));
|
||||||
it('should reject calling a uncallable member',
|
it('should reject calling a uncallable member',
|
||||||
() => reject('{{person().name.first}}', 'Member \'person\' is not callable'));
|
() => reject('{{person().name.first}}', '\'person\' is not callable'));
|
||||||
it('should accept an event handler',
|
it('should accept an event handler',
|
||||||
() => accept('<div (click)="click($event)">{{person.name.first}}</div>'));
|
() => accept('<div (click)="click($event)">{{person.name.first}}</div>'));
|
||||||
it('should reject a misspelled event handler',
|
it('should reject a misspelled event handler',
|
||||||
|
|
Loading…
Reference in New Issue