From 53fc2ed8bf345222e0c3d53ce7f13a4f27f3052e Mon Sep 17 00:00:00 2001 From: ayazhafiz Date: Tue, 12 Nov 2019 15:57:44 -0600 Subject: [PATCH] feat(language-service): completions support for indexed types (#33775) Previously, indexing a container type would not return completions for the indexed type because for every TypeScript type, the recorded index type was always marked as `undefined`, regardless of the index signature. This PR now returns the index type of TypeScript containers with numeric or string index signatures. This allows use to generate completions for arrays and defined index types: ```typescript interface Container { [key: string]: T; } const ctr: Container; ctr['stringKey']. // gives `T.` completions const arr: T[]; arr[0]. // gives `T.` completions ``` Note that this does _not_ provide completions for properties indexed by string literals, e.g. ```typescript interface Container { foo: T; } const ctr: Container; ctr['foo']. // does not give `T.` completions ``` Closes angular/vscode-ng-language-service#110 Closes angular/vscode-ng-language-service#277 PR Close #33775 --- .../language-service/src/typescript_symbols.ts | 15 ++++++++++++++- .../language-service/test/completions_spec.ts | 14 ++++++++++++++ .../language-service/test/diagnostics_spec.ts | 9 +++++++++ .../test/project/app/parsing-cases.ts | 1 + 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/language-service/src/typescript_symbols.ts b/packages/language-service/src/typescript_symbols.ts index 6635ee5a18..73c7dfe28e 100644 --- a/packages/language-service/src/typescript_symbols.ts +++ b/packages/language-service/src/typescript_symbols.ts @@ -280,7 +280,20 @@ class TypeWrapper implements Symbol { return selectSignature(this.tsType, this.context, types); } - indexed(argument: Symbol): Symbol|undefined { return undefined; } + indexed(argument: Symbol): Symbol|undefined { + const type = argument instanceof TypeWrapper ? argument : argument.type; + if (!(type instanceof TypeWrapper)) return; + + const typeKind = typeKindOf(type.tsType); + switch (typeKind) { + case BuiltinType.Number: + const nType = this.tsType.getNumberIndexType(); + return nType && new TypeWrapper(nType, this.context); + case BuiltinType.String: + const sType = this.tsType.getStringIndexType(); + return sType && new TypeWrapper(sType, this.context); + } + } } class SymbolWrapper implements Symbol { diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts index 94ce983126..5dc83ef417 100644 --- a/packages/language-service/test/completions_spec.ts +++ b/packages/language-service/test/completions_spec.ts @@ -117,6 +117,20 @@ describe('completions', () => { expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); }); + it('should be able to get property completions for members in an array', () => { + mockHost.override(TEST_TEMPLATE, `{{ heroes[0].~{heroes-number-index}}}`); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-number-index'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); + }); + + it('should be able to get property completions for members in an indexed type', () => { + mockHost.override(TEST_TEMPLATE, `{{ heroesByName['Jacky'].~{heroes-string-index}}}`); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-string-index'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); + }); + it('should be able to return attribute names with an incompete attribute', () => { const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'no-value-attribute'); const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); diff --git a/packages/language-service/test/diagnostics_spec.ts b/packages/language-service/test/diagnostics_spec.ts index edd0ec103a..ad0127d3ce 100644 --- a/packages/language-service/test/diagnostics_spec.ts +++ b/packages/language-service/test/diagnostics_spec.ts @@ -119,6 +119,15 @@ describe('diagnostics', () => { expect(diagnostics).toEqual([]); }); + it('should produce diagnostics for invalid index type property access', () => { + mockHost.override(TEST_TEMPLATE, ` + {{heroes[0].badProperty}}`); + const diags = ngLS.getDiagnostics(TEST_TEMPLATE); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`); + }); + describe('in expression-cases.ts', () => { it('should report access to an unknown field', () => { const diags = ngLS.getDiagnostics(EXPRESSION_CASES).map(d => d.messageText); diff --git a/packages/language-service/test/project/app/parsing-cases.ts b/packages/language-service/test/project/app/parsing-cases.ts index 1fe3aa7ab6..e9bd88f136 100644 --- a/packages/language-service/test/project/app/parsing-cases.ts +++ b/packages/language-service/test/project/app/parsing-cases.ts @@ -190,6 +190,7 @@ export class TemplateReference { hero: Hero = {id: 1, name: 'Windstorm'}; heroes: Hero[] = [this.hero]; league: Hero[][] = [this.heroes]; + heroesByName: {[name: string]: Hero} = {}; anyValue: any; myClick(event: any) {} }