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