feat(language-service): warn when a method isn't called in an event (#13437)
Closes 13435
This commit is contained in:
parent
4b3d135193
commit
9ec0a4e105
@ -208,8 +208,9 @@ class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
|
|||||||
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) {
|
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) {
|
||||||
const scope = this.getExpressionScope(this.path, includeEvent);
|
const scope = this.getExpressionScope(this.path, includeEvent);
|
||||||
this.diagnostics.push(
|
this.diagnostics.push(
|
||||||
...getExpressionDiagnostics(scope, ast, this.info.template.query)
|
...getExpressionDiagnostics(scope, ast, this.info.template.query, {
|
||||||
.map(d => ({
|
event: includeEvent
|
||||||
|
}).map(d => ({
|
||||||
span: offsetSpan(d.ast.span, offset + this.info.template.span.start),
|
span: offsetSpan(d.ast.span, offset + this.info.template.span.start),
|
||||||
kind: d.kind,
|
kind: d.kind,
|
||||||
message: d.message
|
message: d.message
|
||||||
|
@ -16,9 +16,12 @@ import {TemplateAstChildVisitor, TemplateAstPath} from './template_path';
|
|||||||
import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types';
|
import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types';
|
||||||
import {inSpan, spanOf} from './utils';
|
import {inSpan, spanOf} from './utils';
|
||||||
|
|
||||||
|
export interface ExpressionDiagnosticsContext { event?: boolean; }
|
||||||
|
|
||||||
export function getExpressionDiagnostics(
|
export function getExpressionDiagnostics(
|
||||||
scope: SymbolTable, ast: AST, query: SymbolQuery): TypeDiagnostic[] {
|
scope: SymbolTable, ast: AST, query: SymbolQuery,
|
||||||
const analyzer = new AstType(scope, query);
|
context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] {
|
||||||
|
const analyzer = new AstType(scope, query, context);
|
||||||
analyzer.getDiagnostics(ast);
|
analyzer.getDiagnostics(ast);
|
||||||
return analyzer.diagnostics;
|
return analyzer.diagnostics;
|
||||||
}
|
}
|
||||||
@ -30,7 +33,7 @@ export function getExpressionCompletions(
|
|||||||
const tail = path.tail;
|
const tail = path.tail;
|
||||||
let result: SymbolTable|undefined = scope;
|
let result: SymbolTable|undefined = scope;
|
||||||
|
|
||||||
function getType(ast: AST): Symbol { return new AstType(scope, query).getType(ast); }
|
function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }
|
||||||
|
|
||||||
// If the completion request is in a not in a pipe or property access then the global scope
|
// If the completion request is in a not in a pipe or property access then the global scope
|
||||||
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
|
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
|
||||||
@ -88,7 +91,7 @@ export function getExpressionSymbol(
|
|||||||
if (path.empty) return undefined;
|
if (path.empty) return undefined;
|
||||||
const tail = path.tail;
|
const tail = path.tail;
|
||||||
|
|
||||||
function getType(ast: AST): Symbol { return new AstType(scope, query).getType(ast); }
|
function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }
|
||||||
|
|
||||||
let symbol: Symbol = undefined;
|
let symbol: Symbol = undefined;
|
||||||
let span: Span = undefined;
|
let span: Span = undefined;
|
||||||
@ -189,13 +192,18 @@ export class TypeDiagnostic {
|
|||||||
class AstType implements ExpressionVisitor {
|
class AstType implements ExpressionVisitor {
|
||||||
public diagnostics: TypeDiagnostic[];
|
public diagnostics: TypeDiagnostic[];
|
||||||
|
|
||||||
constructor(private scope: SymbolTable, private query: SymbolQuery) {}
|
constructor(
|
||||||
|
private scope: SymbolTable, private query: SymbolQuery,
|
||||||
|
private context: ExpressionDiagnosticsContext) {}
|
||||||
|
|
||||||
getType(ast: AST): Symbol { return ast.visit(this); }
|
getType(ast: AST): Symbol { return ast.visit(this); }
|
||||||
|
|
||||||
getDiagnostics(ast: AST): TypeDiagnostic[] {
|
getDiagnostics(ast: AST): TypeDiagnostic[] {
|
||||||
this.diagnostics = [];
|
this.diagnostics = [];
|
||||||
ast.visit(this);
|
const type: Symbol = ast.visit(this);
|
||||||
|
if (this.context.event && type.callable) {
|
||||||
|
this.reportWarning('Unexpected callable expression. Expected a method call', ast);
|
||||||
|
}
|
||||||
return this.diagnostics;
|
return this.diagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -746,7 +754,7 @@ function refinedVariableType(
|
|||||||
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
|
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
|
||||||
if (ngForOfBinding) {
|
if (ngForOfBinding) {
|
||||||
const bindingType =
|
const bindingType =
|
||||||
new AstType(info.template.members, info.template.query).getType(ngForOfBinding.value);
|
new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value);
|
||||||
if (bindingType) {
|
if (bindingType) {
|
||||||
return info.template.query.getElementType(bindingType);
|
return info.template.query.getElementType(bindingType);
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,17 @@ describe('diagnostics', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should report a warning if an event results in a callable expression', () => {
|
||||||
|
const code =
|
||||||
|
` @Component({template: \`<div (click)="onClick"></div>\`}) export class MyComponent { onClick() { } }`;
|
||||||
|
addCode(code, (fileName, content) => {
|
||||||
|
const diagnostics = ngService.getDiagnostics(fileName);
|
||||||
|
includeDiagnostic(
|
||||||
|
diagnostics, 'Unexpected callable expression. Expected a method call', 'onClick',
|
||||||
|
content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
|
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
|
||||||
const fileName = '/app/app.component.ts';
|
const fileName = '/app/app.component.ts';
|
||||||
const originalContent = mockHost.getFileContent(fileName);
|
const originalContent = mockHost.getFileContent(fileName);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user