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) { |   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 | ||||||
|                    span: offsetSpan(d.ast.span, offset + this.info.template.span.start), |         }).map(d => ({ | ||||||
|                    kind: d.kind, |                  span: offsetSpan(d.ast.span, offset + this.info.template.span.start), | ||||||
|                    message: d.message |                  kind: d.kind, | ||||||
|                  }))); |                  message: d.message | ||||||
|  |                }))); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private push(ast: TemplateAst) { this.path.push(ast); } |   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 {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