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
This commit is contained in:
Ayaz Hafiz 2020-03-30 00:10:34 -07:00 committed by Alex Rickabaugh
parent 38ad1d97ab
commit fe2b6923ba
3 changed files with 49 additions and 13 deletions

View File

@ -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];
}
}

View File

@ -96,17 +96,49 @@ describe('hover', () => {
expect(documentation).toBe('This is the title of the `TemplateReference` Component.');
});
it('should work for property reads', () => {
describe('property reads', () => {
it('should work for class members', () => {
mockHost.override(TEST_TEMPLATE, `<div>{{«title»}}</div>`);
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');
});
it('should work for array members', () => {
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let hero of heroes">{{«hero»}}</div>`);
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, `<div *ngFor="let hero of readonlyHeroes">{{«hero»}}</div>`);
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<Hero>');
});
it('should work for const array members (#36191)', () => {
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let name of constNames">{{«name»}}</div>`);
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', () => {
mockHost.override(TEST_TEMPLATE, `<div (click)="«ᐱmyClickᐱ($event)»"></div>`);
const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'myClick');

View File

@ -174,6 +174,8 @@ export class TemplateReference {
index = null;
myClick(event: any) {}
birthday = new Date();
readonlyHeroes: ReadonlyArray<Readonly<Hero>> = this.heroes;
constNames = [{name: 'name'}] as const ;
}
@Component({