fix(language-service): Add global symbol for $any() (#33245)
This commit introduces a "global symbol table" in the language service for symbols that are available in the top level scope, and add `$any()` to it. See https://angular.io/guide/template-syntax#the-any-type-cast-function PR closes https://github.com/angular/vscode-ng-language-service/issues/242 PR Close #33245
This commit is contained in:
		
							parent
							
								
									8bc5fb2ab6
								
							
						
					
					
						commit
						3f257e96c6
					
				
							
								
								
									
										63
									
								
								packages/language-service/src/global_symbols.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								packages/language-service/src/global_symbols.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | /** | ||||||
|  |  * @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 ng from '../src/types'; | ||||||
|  | 
 | ||||||
|  | export const EMPTY_SYMBOL_TABLE: Readonly<ng.SymbolTable> = { | ||||||
|  |   size: 0, | ||||||
|  |   get: () => undefined, | ||||||
|  |   has: () => false, | ||||||
|  |   values: () => [], | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A factory function that returns a symbol table that contains all global symbols | ||||||
|  |  * available in an interpolation scope in a template. | ||||||
|  |  * This function creates the table the first time it is called, and return a cached | ||||||
|  |  * value for all subsequent calls. | ||||||
|  |  */ | ||||||
|  | export const createGlobalSymbolTable: (query: ng.SymbolQuery) => ng.SymbolTable = (function() { | ||||||
|  |   let GLOBAL_SYMBOL_TABLE: ng.SymbolTable|undefined; | ||||||
|  |   return function(query: ng.SymbolQuery) { | ||||||
|  |     if (GLOBAL_SYMBOL_TABLE) { | ||||||
|  |       return GLOBAL_SYMBOL_TABLE; | ||||||
|  |     } | ||||||
|  |     GLOBAL_SYMBOL_TABLE = query.createSymbolTable([ | ||||||
|  |       // The `$any()` method casts the type of an expression to `any`.
 | ||||||
|  |       // https://angular.io/guide/template-syntax#the-any-type-cast-function
 | ||||||
|  |       { | ||||||
|  |         name: '$any', | ||||||
|  |         kind: 'method', | ||||||
|  |         type: { | ||||||
|  |           name: '$any', | ||||||
|  |           kind: 'method', | ||||||
|  |           type: undefined, | ||||||
|  |           language: 'typescript', | ||||||
|  |           container: undefined, | ||||||
|  |           public: true, | ||||||
|  |           callable: true, | ||||||
|  |           definition: undefined, | ||||||
|  |           nullable: false, | ||||||
|  |           members: () => EMPTY_SYMBOL_TABLE, | ||||||
|  |           signatures: () => [], | ||||||
|  |           selectSignature(args: ng.Symbol[]) { | ||||||
|  |             if (args.length !== 1) { | ||||||
|  |               return; | ||||||
|  |             } | ||||||
|  |             return { | ||||||
|  |               arguments: EMPTY_SYMBOL_TABLE,  // not used
 | ||||||
|  |               result: query.getBuiltinType(ng.BuiltinType.Any), | ||||||
|  |             }; | ||||||
|  |           }, | ||||||
|  |           indexed: () => undefined, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     ]); | ||||||
|  |     return GLOBAL_SYMBOL_TABLE; | ||||||
|  |   }; | ||||||
|  | })(); | ||||||
| @ -10,6 +10,7 @@ import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@an | |||||||
| import * as ts from 'typescript'; | import * as ts from 'typescript'; | ||||||
| 
 | 
 | ||||||
| import {isAstResult} from './common'; | import {isAstResult} from './common'; | ||||||
|  | import {createGlobalSymbolTable} from './global_symbols'; | ||||||
| import * as ng from './types'; | import * as ng from './types'; | ||||||
| import {TypeScriptServiceHost} from './typescript_host'; | import {TypeScriptServiceHost} from './typescript_host'; | ||||||
| 
 | 
 | ||||||
| @ -48,8 +49,10 @@ abstract class BaseTemplate implements ng.TemplateSource { | |||||||
|     if (!this.membersTable) { |     if (!this.membersTable) { | ||||||
|       const typeChecker = this.program.getTypeChecker(); |       const typeChecker = this.program.getTypeChecker(); | ||||||
|       const sourceFile = this.classDeclNode.getSourceFile(); |       const sourceFile = this.classDeclNode.getSourceFile(); | ||||||
|       this.membersTable = |       this.membersTable = this.query.mergeSymbolTable([ | ||||||
|           getClassMembersFromDeclaration(this.program, typeChecker, sourceFile, this.classDeclNode); |         createGlobalSymbolTable(this.query), | ||||||
|  |         getClassMembersFromDeclaration(this.program, typeChecker, sourceFile, this.classDeclNode), | ||||||
|  |       ]); | ||||||
|     } |     } | ||||||
|     return this.membersTable; |     return this.membersTable; | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ ts_library( | |||||||
|     deps = [ |     deps = [ | ||||||
|         "//packages:types", |         "//packages:types", | ||||||
|         "//packages/compiler", |         "//packages/compiler", | ||||||
|  |         "//packages/compiler-cli", | ||||||
|         "//packages/compiler-cli/test:test_utils", |         "//packages/compiler-cli/test:test_utils", | ||||||
|         "//packages/language-service", |         "//packages/language-service", | ||||||
|         "@npm//typescript", |         "@npm//typescript", | ||||||
| @ -21,7 +22,6 @@ jasmine_node_test( | |||||||
|     name = "test", |     name = "test", | ||||||
|     data = [ |     data = [ | ||||||
|         "//packages/common:npm_package", |         "//packages/common:npm_package", | ||||||
|         "//packages/compiler:npm_package", |  | ||||||
|         "//packages/core:npm_package", |         "//packages/core:npm_package", | ||||||
|         "//packages/forms:npm_package", |         "//packages/forms:npm_package", | ||||||
|     ], |     ], | ||||||
|  | |||||||
| @ -153,6 +153,12 @@ describe('completions', () => { | |||||||
|     expectContain(completions, CompletionKind.PROPERTY, ['title', 'subTitle']); |     expectContain(completions, CompletionKind.PROPERTY, ['title', 'subTitle']); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   it('should suggest $any() type cast function in an interpolation', () => { | ||||||
|  |     const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'sub-start'); | ||||||
|  |     const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start); | ||||||
|  |     expectContain(completions, CompletionKind.METHOD, ['$any']); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   describe('in external template', () => { |   describe('in external template', () => { | ||||||
|     it('should be able to get entity completions in external template', () => { |     it('should be able to get entity completions in external template', () => { | ||||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'entity-amp'); |       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'entity-amp'); | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ import {MockTypescriptHost} from './test_utils'; | |||||||
| const EXPRESSION_CASES = '/app/expression-cases.ts'; | const EXPRESSION_CASES = '/app/expression-cases.ts'; | ||||||
| const NG_FOR_CASES = '/app/ng-for-cases.ts'; | const NG_FOR_CASES = '/app/ng-for-cases.ts'; | ||||||
| const NG_IF_CASES = '/app/ng-if-cases.ts'; | const NG_IF_CASES = '/app/ng-if-cases.ts'; | ||||||
|  | const TEST_TEMPLATE = '/app/test.ng'; | ||||||
| 
 | 
 | ||||||
| describe('diagnostics', () => { | describe('diagnostics', () => { | ||||||
|   const mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']); |   const mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']); | ||||||
| @ -55,6 +56,26 @@ describe('diagnostics', () => { | |||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   // https://github.com/angular/vscode-ng-language-service/issues/242
 | ||||||
|  |   it('should support $any() type cast function', () => { | ||||||
|  |     mockHost.override(TEST_TEMPLATE, `<div>{{$any(title).xyz}}</div>`); | ||||||
|  |     const diags = ngLS.getDiagnostics(TEST_TEMPLATE); | ||||||
|  |     expect(diags).toEqual([]); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should report error for $any() with incorrect number of arguments', () => { | ||||||
|  |     const templates = [ | ||||||
|  |       '<div>{{$any().xyz}}</div>',              // no argument
 | ||||||
|  |       '<div>{{$any(title, title).xyz}}</div>',  // two arguments
 | ||||||
|  |     ]; | ||||||
|  |     for (const template of templates) { | ||||||
|  |       mockHost.override(TEST_TEMPLATE, template); | ||||||
|  |       const diags = ngLS.getDiagnostics(TEST_TEMPLATE); | ||||||
|  |       expect(diags.length).toBe(1); | ||||||
|  |       expect(diags[0].messageText).toBe('Unable to resolve signature for call of method $any'); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   describe('in expression-cases.ts', () => { |   describe('in expression-cases.ts', () => { | ||||||
|     it('should report access to an unknown field', () => { |     it('should report access to an unknown field', () => { | ||||||
|       const diags = ngLS.getDiagnostics(EXPRESSION_CASES).map(d => d.messageText); |       const diags = ngLS.getDiagnostics(EXPRESSION_CASES).map(d => d.messageText); | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								packages/language-service/test/global_symbols_spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								packages/language-service/test/global_symbols_spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 {getSymbolQuery} from '@angular/compiler-cli'; | ||||||
|  | import * as ts from 'typescript/lib/tsserverlibrary'; | ||||||
|  | 
 | ||||||
|  | import {EMPTY_SYMBOL_TABLE, createGlobalSymbolTable} from '../src/global_symbols'; | ||||||
|  | 
 | ||||||
|  | import {MockTypescriptHost} from './test_utils'; | ||||||
|  | 
 | ||||||
|  | describe('GlobalSymbolTable', () => { | ||||||
|  |   const mockHost = new MockTypescriptHost([]); | ||||||
|  |   const tsLS = ts.createLanguageService(mockHost); | ||||||
|  | 
 | ||||||
|  |   it(`contains $any()`, () => { | ||||||
|  |     const program = tsLS.getProgram() !; | ||||||
|  |     const typeChecker = program.getTypeChecker(); | ||||||
|  |     const source = ts.createSourceFile('foo.ts', '', ts.ScriptTarget.ES2015); | ||||||
|  |     const query = getSymbolQuery(program, typeChecker, source, () => EMPTY_SYMBOL_TABLE); | ||||||
|  |     const table = createGlobalSymbolTable(query); | ||||||
|  |     expect(table.has('$any')).toBe(true); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -13,6 +13,8 @@ import {TypeScriptServiceHost} from '../src/typescript_host'; | |||||||
| 
 | 
 | ||||||
| import {MockTypescriptHost} from './test_utils'; | import {MockTypescriptHost} from './test_utils'; | ||||||
| 
 | 
 | ||||||
|  | const TEST_TEMPLATE = '/app/test.ng'; | ||||||
|  | 
 | ||||||
| describe('hover', () => { | describe('hover', () => { | ||||||
|   const mockHost = new MockTypescriptHost(['/app/main.ts']); |   const mockHost = new MockTypescriptHost(['/app/main.ts']); | ||||||
|   const tsLS = ts.createLanguageService(mockHost); |   const tsLS = ts.createLanguageService(mockHost); | ||||||
| @ -190,6 +192,19 @@ describe('hover', () => { | |||||||
|     }); |     }); | ||||||
|     expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: class'); |     expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: class'); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   it('should be able to provide quick info for $any() cast function', () => { | ||||||
|  |     const content = mockHost.override(TEST_TEMPLATE, '<div>{{$any(title)}}</div>'); | ||||||
|  |     const position = content.indexOf('$any'); | ||||||
|  |     const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, position); | ||||||
|  |     expect(quickInfo).toBeDefined(); | ||||||
|  |     const {textSpan, displayParts} = quickInfo !; | ||||||
|  |     expect(textSpan).toEqual({ | ||||||
|  |       start: position, | ||||||
|  |       length: '$any(title)'.length, | ||||||
|  |     }); | ||||||
|  |     expect(toText(displayParts)).toBe('(method) $any'); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| function toText(displayParts?: ts.SymbolDisplayPart[]): string { | function toText(displayParts?: ts.SymbolDisplayPart[]): string { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user