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,12 +208,13 @@ class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor { | ||||
|   private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) { | ||||
|     const scope = this.getExpressionScope(this.path, includeEvent); | ||||
|     this.diagnostics.push( | ||||
|         ...getExpressionDiagnostics(scope, ast, this.info.template.query) | ||||
|             .map(d => ({ | ||||
|                    span: offsetSpan(d.ast.span, offset + this.info.template.span.start), | ||||
|                    kind: d.kind, | ||||
|                    message: d.message | ||||
|                  }))); | ||||
|         ...getExpressionDiagnostics(scope, ast, this.info.template.query, { | ||||
|           event: includeEvent | ||||
|         }).map(d => ({ | ||||
|                  span: offsetSpan(d.ast.span, offset + this.info.template.span.start), | ||||
|                  kind: d.kind, | ||||
|                  message: d.message | ||||
|                }))); | ||||
|   } | ||||
| 
 | ||||
|   private push(ast: TemplateAst) { this.path.push(ast); } | ||||
|  | ||||
| @ -16,9 +16,12 @@ import {TemplateAstChildVisitor, TemplateAstPath} from './template_path'; | ||||
| import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types'; | ||||
| import {inSpan, spanOf} from './utils'; | ||||
| 
 | ||||
| export interface ExpressionDiagnosticsContext { event?: boolean; } | ||||
| 
 | ||||
| export function getExpressionDiagnostics( | ||||
|     scope: SymbolTable, ast: AST, query: SymbolQuery): TypeDiagnostic[] { | ||||
|   const analyzer = new AstType(scope, query); | ||||
|     scope: SymbolTable, ast: AST, query: SymbolQuery, | ||||
|     context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] { | ||||
|   const analyzer = new AstType(scope, query, context); | ||||
|   analyzer.getDiagnostics(ast); | ||||
|   return analyzer.diagnostics; | ||||
| } | ||||
| @ -30,7 +33,7 @@ export function getExpressionCompletions( | ||||
|   const tail = path.tail; | ||||
|   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
 | ||||
|   // (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; | ||||
|   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 span: Span = undefined; | ||||
| @ -189,13 +192,18 @@ export class TypeDiagnostic { | ||||
| class AstType implements ExpressionVisitor { | ||||
|   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); } | ||||
| 
 | ||||
|   getDiagnostics(ast: AST): TypeDiagnostic[] { | ||||
|     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; | ||||
|   } | ||||
| 
 | ||||
| @ -746,7 +754,7 @@ function refinedVariableType( | ||||
|     const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf'); | ||||
|     if (ngForOfBinding) { | ||||
|       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) { | ||||
|         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) { | ||||
|       const fileName = '/app/app.component.ts'; | ||||
|       const originalContent = mockHost.getFileContent(fileName); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user