From fe2b6923baee06f7348005ce76c4640b776ec0da Mon Sep 17 00:00:00 2001 From: Ayaz Hafiz Date: Mon, 30 Mar 2020 00:10:34 -0700 Subject: [PATCH] fix(language-service): infer type of elements of array-like objects (#36312) Currently the language service only provides support for determining the type of array-like members when the array-like object is an `Array`. However, there are other kinds of array-like objects, including `ReadonlyArray`s and `readonly`-property arrays. This commit adds support for retrieving the element type of arbitrary array-like objects. Closes #36191 PR Close #36312 --- .../src/typescript_symbols.ts | 10 ++-- packages/language-service/test/hover_spec.ts | 50 +++++++++++++++---- .../test/project/app/parsing-cases.ts | 2 + 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/language-service/src/typescript_symbols.ts b/packages/language-service/src/typescript_symbols.ts index 69812f295f..f018a092c6 100644 --- a/packages/language-service/src/typescript_symbols.ts +++ b/packages/language-service/src/typescript_symbols.ts @@ -127,10 +127,12 @@ class TypeScriptSymbolQuery implements SymbolQuery { getElementType(type: Symbol): Symbol|undefined { if (type instanceof TypeWrapper) { - const tSymbol = type.tsType.symbol; - const tArgs = type.typeArguments(); - if (!tSymbol || tSymbol.name !== 'Array' || !tArgs || tArgs.length != 1) return; - return tArgs[0]; + const ty = type.tsType; + const tyArgs = type.typeArguments(); + // TODO(ayazhafiz): Track https://github.com/microsoft/TypeScript/issues/37711 to expose + // `isArrayLikeType` as a public method. + if (!(this.checker as any).isArrayLikeType(ty) || tyArgs?.length !== 1) return; + return tyArgs[0]; } } diff --git a/packages/language-service/test/hover_spec.ts b/packages/language-service/test/hover_spec.ts index dc3bb7098b..9a22801c06 100644 --- a/packages/language-service/test/hover_spec.ts +++ b/packages/language-service/test/hover_spec.ts @@ -96,15 +96,47 @@ describe('hover', () => { expect(documentation).toBe('This is the title of the `TemplateReference` Component.'); }); - it('should work for property reads', () => { - mockHost.override(TEST_TEMPLATE, `
{{«title»}}
`); - const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); - const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); - expect(quickInfo).toBeTruthy(); - const {textSpan, displayParts} = quickInfo !; - expect(textSpan).toEqual(marker); - expect(textSpan.length).toBe('title'.length); - expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); + describe('property reads', () => { + it('should work for class members', () => { + mockHost.override(TEST_TEMPLATE, `
{{«title»}}
`); + const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); + const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); + expect(quickInfo).toBeTruthy(); + const {textSpan, displayParts} = quickInfo !; + expect(textSpan).toEqual(marker); + expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); + }); + + it('should work for array members', () => { + mockHost.override(TEST_TEMPLATE, `
{{«hero»}}
`); + const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'hero'); + const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); + expect(quickInfo).toBeTruthy(); + const {textSpan, displayParts} = quickInfo !; + expect(textSpan).toEqual(marker); + expect(toText(displayParts)).toBe('(variable) hero: Hero'); + }); + + it('should work for ReadonlyArray members (#36191)', () => { + mockHost.override( + TEST_TEMPLATE, `
{{«hero»}}
`); + const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'hero'); + const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); + expect(quickInfo).toBeTruthy(); + const {textSpan, displayParts} = quickInfo !; + expect(textSpan).toEqual(marker); + expect(toText(displayParts)).toBe('(variable) hero: Readonly'); + }); + + it('should work for const array members (#36191)', () => { + mockHost.override(TEST_TEMPLATE, `
{{«name»}}
`); + const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'name'); + const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); + expect(quickInfo).toBeTruthy(); + const {textSpan, displayParts} = quickInfo !; + expect(textSpan).toEqual(marker); + expect(toText(displayParts)).toBe('(variable) name: { readonly name: "name"; }'); + }); }); it('should work for method calls', () => { diff --git a/packages/language-service/test/project/app/parsing-cases.ts b/packages/language-service/test/project/app/parsing-cases.ts index 1d5c01c70e..3239e008cf 100644 --- a/packages/language-service/test/project/app/parsing-cases.ts +++ b/packages/language-service/test/project/app/parsing-cases.ts @@ -174,6 +174,8 @@ export class TemplateReference { index = null; myClick(event: any) {} birthday = new Date(); + readonlyHeroes: ReadonlyArray> = this.heroes; + constNames = [{name: 'name'}] as const ; } @Component({