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 {isAstResult} from './common'; | ||||
| import {createGlobalSymbolTable} from './global_symbols'; | ||||
| import * as ng from './types'; | ||||
| import {TypeScriptServiceHost} from './typescript_host'; | ||||
| 
 | ||||
| @ -48,8 +49,10 @@ abstract class BaseTemplate implements ng.TemplateSource { | ||||
|     if (!this.membersTable) { | ||||
|       const typeChecker = this.program.getTypeChecker(); | ||||
|       const sourceFile = this.classDeclNode.getSourceFile(); | ||||
|       this.membersTable = | ||||
|           getClassMembersFromDeclaration(this.program, typeChecker, sourceFile, this.classDeclNode); | ||||
|       this.membersTable = this.query.mergeSymbolTable([ | ||||
|         createGlobalSymbolTable(this.query), | ||||
|         getClassMembersFromDeclaration(this.program, typeChecker, sourceFile, this.classDeclNode), | ||||
|       ]); | ||||
|     } | ||||
|     return this.membersTable; | ||||
|   } | ||||
|  | ||||
| @ -11,6 +11,7 @@ ts_library( | ||||
|     deps = [ | ||||
|         "//packages:types", | ||||
|         "//packages/compiler", | ||||
|         "//packages/compiler-cli", | ||||
|         "//packages/compiler-cli/test:test_utils", | ||||
|         "//packages/language-service", | ||||
|         "@npm//typescript", | ||||
| @ -21,7 +22,6 @@ jasmine_node_test( | ||||
|     name = "test", | ||||
|     data = [ | ||||
|         "//packages/common:npm_package", | ||||
|         "//packages/compiler:npm_package", | ||||
|         "//packages/core:npm_package", | ||||
|         "//packages/forms:npm_package", | ||||
|     ], | ||||
|  | ||||
| @ -153,6 +153,12 @@ describe('completions', () => { | ||||
|     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', () => { | ||||
|     it('should be able to get entity completions in external template', () => { | ||||
|       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 NG_FOR_CASES = '/app/ng-for-cases.ts'; | ||||
| const NG_IF_CASES = '/app/ng-if-cases.ts'; | ||||
| const TEST_TEMPLATE = '/app/test.ng'; | ||||
| 
 | ||||
| describe('diagnostics', () => { | ||||
|   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', () => { | ||||
|     it('should report access to an unknown field', () => { | ||||
|       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'; | ||||
| 
 | ||||
| const TEST_TEMPLATE = '/app/test.ng'; | ||||
| 
 | ||||
| describe('hover', () => { | ||||
|   const mockHost = new MockTypescriptHost(['/app/main.ts']); | ||||
|   const tsLS = ts.createLanguageService(mockHost); | ||||
| @ -190,6 +192,19 @@ describe('hover', () => { | ||||
|     }); | ||||
|     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 { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user