Skip to content

Commit

Permalink
feat(language-service): completions support for indexed types (#34047)
Browse files Browse the repository at this point in the history
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<T> {
  [key: string]: T;
}
const ctr: Container<T>;
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<T> {
  foo: T;
}
const ctr: Container<T>;
ctr['foo']. // does not give `T.` completions
```

Closes angular/vscode-ng-language-service#110
Closes angular/vscode-ng-language-service#277

PR Close #34047
  • Loading branch information
ayazhafiz authored and matsko committed Nov 26, 2019
1 parent d228801 commit 8a565c8
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 1 deletion.
15 changes: 14 additions & 1 deletion packages/language-service/src/typescript_symbols.ts
Expand Up @@ -284,7 +284,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 {
Expand Down
14 changes: 14 additions & 0 deletions packages/language-service/test/completions_spec.ts
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions packages/language-service/test/diagnostics_spec.ts
Expand Up @@ -127,6 +127,15 @@ describe('diagnostics', () => {
expect(diags).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);
Expand Down
Expand Up @@ -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) {}
}
Expand Down

0 comments on commit 8a565c8

Please sign in to comment.