From 7faa9bbc093ee466d62d3020c594d4b7764c9d93 Mon Sep 17 00:00:00 2001 From: ivanwonder Date: Wed, 20 Nov 2019 07:23:45 +0000 Subject: [PATCH] feat(language-service): completions support for tuple array (#33928) PR Close #33928 --- .../language-service/src/expression_type.ts | 3 ++- packages/language-service/src/symbols.ts | 7 +++++-- .../language-service/src/typescript_symbols.ts | 11 +++++++++-- .../language-service/test/completions_spec.ts | 7 +++++++ .../language-service/test/diagnostics_spec.ts | 17 +++++++++++++++++ .../test/project/app/parsing-cases.ts | 1 + 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/language-service/src/expression_type.ts b/packages/language-service/src/expression_type.ts index 359ed1c385..1aee6ed289 100644 --- a/packages/language-service/src/expression_type.ts +++ b/packages/language-service/src/expression_type.ts @@ -241,7 +241,8 @@ export class AstType implements AstVisitor { visitKeyedRead(ast: KeyedRead): Symbol { const targetType = this.getType(ast.obj); const keyType = this.getType(ast.key); - const result = targetType.indexed(keyType); + const result = targetType.indexed( + keyType, ast.key instanceof LiteralPrimitive ? ast.key.value : undefined); return result || this.anyType; } diff --git a/packages/language-service/src/symbols.ts b/packages/language-service/src/symbols.ts index f8ea3ced6e..8a7ea96b3f 100644 --- a/packages/language-service/src/symbols.ts +++ b/packages/language-service/src/symbols.ts @@ -116,9 +116,12 @@ export interface Symbol { /** * Return the type of the expression if this symbol is indexed by `argument`. + * Sometimes we need the key of arguments to get the type of the expression, for example + * in the case of tuples (`type Example = [string, number]`). + * [string, number]). * If the symbol cannot be indexed, this method should return `undefined`. */ - indexed(argument: Symbol): Symbol|undefined; + indexed(argument: Symbol, key?: any): Symbol|undefined; } /** @@ -349,4 +352,4 @@ export interface SymbolQuery { * Return the span of the narrowest non-token node at the given location. */ getSpanAt(line: number, column: number): Span|undefined; -} \ No newline at end of file +} diff --git a/packages/language-service/src/typescript_symbols.ts b/packages/language-service/src/typescript_symbols.ts index 1bae4d0946..2e4343206f 100644 --- a/packages/language-service/src/typescript_symbols.ts +++ b/packages/language-service/src/typescript_symbols.ts @@ -284,7 +284,7 @@ class TypeWrapper implements Symbol { return selectSignature(this.tsType, this.context, types); } - indexed(argument: Symbol): Symbol|undefined { + indexed(argument: Symbol, value: any): Symbol|undefined { const type = argument instanceof TypeWrapper ? argument : argument.type; if (!(type instanceof TypeWrapper)) return; @@ -292,7 +292,14 @@ class TypeWrapper implements Symbol { switch (typeKind) { case BuiltinType.Number: const nType = this.tsType.getNumberIndexType(); - return nType && new TypeWrapper(nType, this.context); + if (nType) { + // get the right tuple type by value, like 'var t: [number, string];' + if (nType.isUnion()) { + return new TypeWrapper(nType.types[value], this.context); + } + return new TypeWrapper(nType, this.context); + } + return undefined; case BuiltinType.String: const sType = this.tsType.getStringIndexType(); return sType && new TypeWrapper(sType, this.context); diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts index 9bee38e345..972cb3db66 100644 --- a/packages/language-service/test/completions_spec.ts +++ b/packages/language-service/test/completions_spec.ts @@ -125,6 +125,13 @@ describe('completions', () => { expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); }); + it('should work with numeric index signatures (tuple arrays)', () => { + mockHost.override(TEST_TEMPLATE, `{{ tupleArray[1].~{tuple-array-number-index}}}`); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'tuple-array-number-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}}}`); diff --git a/packages/language-service/test/diagnostics_spec.ts b/packages/language-service/test/diagnostics_spec.ts index 145cf6860e..3b22121db2 100644 --- a/packages/language-service/test/diagnostics_spec.ts +++ b/packages/language-service/test/diagnostics_spec.ts @@ -158,6 +158,23 @@ describe('diagnostics', () => { }); }); + it('should produce diagnostics for invalid tuple type property access', () => { + mockHost.override(TEST_TEMPLATE, ` + {{tupleArray[1].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()', () => { + mockHost.override(TEST_TEMPLATE, ` + + `); + const diags = ngLS.getDiagnostics(TEST_TEMPLATE); + expect(diags).toEqual([]); + }); + 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 e9bd88f136..6050caa0b1 100644 --- a/packages/language-service/test/project/app/parsing-cases.ts +++ b/packages/language-service/test/project/app/parsing-cases.ts @@ -189,6 +189,7 @@ export class TemplateReference { title = 'Some title'; hero: Hero = {id: 1, name: 'Windstorm'}; heroes: Hero[] = [this.hero]; + tupleArray: [string, Hero] = ['test', this.hero]; league: Hero[][] = [this.heroes]; heroesByName: {[name: string]: Hero} = {}; anyValue: any;