fix(language-service): determine index types accessed using dot notation (#33884)

Commit 53fc2ed8bf added support for
determining index types accessed using index signatures, but did not
include support for index types accessed using dot notation:

```typescript
const obj<T>: { [key: string]: T };
obj['stringKey']. // gets `T.` completions
obj.stringKey.    // did not peviously get `T.` completions
```

This adds support for determining an index type accessed via dot
notation by rigging an object's symbol table to return the string index
signature type a property access refers to, if that property does not
explicitly exist on the object. This is very similar to @ivanwonder's
work in #29811.

`SymbolWrapper` now takes an additional parameter to explicitly set the
type of the symbol wrapped. This is done because
`SymbolTableWrapper#get` only has access to the symbol of the index
type, _not_ the index signature symbol itself. An attempt to get the
type of the index type will give an error.

Closes #29811
Closes https://github.com/angular/vscode-ng-language-service/issues/126

PR Close #33884
This commit is contained in:
ayazhafiz 2019-11-17 15:06:41 -06:00 committed by Miško Hevery
parent 6d7d2e439c
commit 49804fe017
3 changed files with 94 additions and 27 deletions

View File

@ -275,7 +275,7 @@ class TypeWrapper implements Symbol {
// the former includes properties on the base class whereas the latter does
// not. This provides properties like .bind(), .call(), .apply(), etc for
// functions.
return new SymbolTableWrapper(this.tsType.getApparentProperties(), this.context);
return new SymbolTableWrapper(this.tsType.getApparentProperties(), this.context, this.tsType);
}
signatures(): Signature[] { return signaturesOf(this.tsType, this.context); }
@ -302,15 +302,18 @@ class TypeWrapper implements Symbol {
class SymbolWrapper implements Symbol {
private symbol: ts.Symbol;
// TODO(issue/24571): remove '!'.
private _tsType !: ts.Type;
// TODO(issue/24571): remove '!'.
private _members !: SymbolTable;
private _members?: SymbolTable;
public readonly nullable: boolean = false;
public readonly language: string = 'typescript';
constructor(symbol: ts.Symbol, private context: TypeContext) {
constructor(
symbol: ts.Symbol,
/** TypeScript type context of the symbol. */
private context: TypeContext,
/** Type of the TypeScript symbol, if known. If not provided, the type of the symbol
* will be determined dynamically; see `SymbolWrapper#tsType`. */
private _tsType?: ts.Type) {
this.symbol = symbol && context && (symbol.flags & ts.SymbolFlags.Alias) ?
context.checker.getAliasedSymbol(symbol) :
symbol;
@ -340,7 +343,7 @@ class SymbolWrapper implements Symbol {
const typeWrapper = new TypeWrapper(declaredType, this.context);
this._members = typeWrapper.members();
} else {
this._members = new SymbolTableWrapper(this.symbol.members !, this.context);
this._members = new SymbolTableWrapper(this.symbol.members !, this.context, this.tsType);
}
}
return this._members;
@ -454,8 +457,16 @@ function toSymbols(symbolTable: ts.SymbolTable | undefined): ts.Symbol[] {
class SymbolTableWrapper implements SymbolTable {
private symbols: ts.Symbol[];
private symbolTable: ts.SymbolTable;
private stringIndexType?: ts.Type;
constructor(symbols: ts.SymbolTable|ts.Symbol[]|undefined, private context: TypeContext) {
/**
* Creates a queryable table of symbols belonging to a TypeScript entity.
* @param symbols symbols to query belonging to the entity
* @param context program context
* @param type original TypeScript type of entity owning the symbols, if known
*/
constructor(
symbols: ts.SymbolTable|ts.Symbol[], private context: TypeContext, private type?: ts.Type) {
symbols = symbols || [];
if (Array.isArray(symbols)) {
@ -465,18 +476,41 @@ class SymbolTableWrapper implements SymbolTable {
this.symbols = toSymbols(symbols);
this.symbolTable = symbols;
}
if (type) {
this.stringIndexType = type.getStringIndexType();
}
}
get size(): number { return this.symbols.length; }
get(key: string): Symbol|undefined {
const symbol = getFromSymbolTable(this.symbolTable, key);
return symbol ? new SymbolWrapper(symbol, this.context) : undefined;
if (symbol) {
return new SymbolWrapper(symbol, this.context);
}
if (this.stringIndexType) {
// If the key does not exist as an explicit symbol on the type, it may be accessing a string
// index signature using dot notation:
//
// const obj<T>: { [key: string]: T };
// obj.stringIndex // equivalent to obj['stringIndex'];
//
// In this case, return the type indexed by an arbitrary string key.
const symbol = this.stringIndexType.getSymbol();
if (symbol) {
return new SymbolWrapper(symbol, this.context, this.stringIndexType);
}
}
return undefined;
}
has(key: string): boolean {
const table: any = this.symbolTable;
return (typeof table.has === 'function') ? table.has(key) : table[key] != null;
return ((typeof table.has === 'function') ? table.has(key) : table[key] != null) ||
this.stringIndexType !== undefined;
}
values(): Symbol[] { return this.symbols.map(s => new SymbolWrapper(s, this.context)); }

View File

@ -117,18 +117,29 @@ 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']);
});
describe('property completions for members of an indexed type', () => {
it('should work with numeric index signatures (arrays)', () => {
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']);
describe('with string index signatures', () => {
it('should work with index notation', () => {
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 work with dot notation', () => {
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', () => {

View File

@ -119,13 +119,35 @@ describe('diagnostics', () => {
expect(diagnostics).toEqual([]);
});
it('should produce diagnostics for invalid index type property access', () => {
mockHost.override(TEST_TEMPLATE, `
describe('diagnostics for invalid indexed type property access', () => {
it('should work with numeric index signatures (arrays)', () => {
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`);
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('with string index signatures', () => {
it('should work with index notation', () => {
mockHost.override(TEST_TEMPLATE, `
{{heroesByName['Jacky'].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`);
});
it('should work with dot notation', () => {
mockHost.override(TEST_TEMPLATE, `
{{heroesByName.jacky.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`);
});
});
});
it('should not produce errors on function.bind()', () => {