From 9557e4ad961c97e553e85e94a08f39ff72d3218e Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 23 Mar 2018 16:04:29 -0700 Subject: [PATCH] Handle completions in interface / type literal similar to class (#22701) * Handle completions in interface / type literal similar to class * Code review --- src/compiler/types.ts | 6 +- src/compiler/utilities.ts | 6 +- src/services/completions.ts | 190 ++++++++---------- .../reference/api/tsserverlibrary.d.ts | 5 +- tests/baselines/reference/api/typescript.d.ts | 5 +- ...ierDefinitionLocations_interfaceMembers.ts | 7 - ...erDefinitionLocations_interfaceMembers2.ts | 7 - ...erDefinitionLocations_interfaceMembers3.ts | 7 - ...nUnclosedObjectTypeLiteralInSignature04.ts | 8 +- ...pletionListWithModulesInsideModuleScope.ts | 4 +- .../fourslash/completionsInterfaceElement.ts | 18 ++ 11 files changed, 120 insertions(+), 143 deletions(-) delete mode 100644 tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers.ts delete mode 100644 tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers2.ts delete mode 100644 tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers3.ts create mode 100644 tests/cases/fourslash/completionsInterfaceElement.ts diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5da13fd2d59..99e94f2d576 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -990,7 +990,7 @@ namespace ts { export interface MethodSignature extends SignatureDeclarationBase, TypeElement { kind: SyntaxKind.MethodSignature; - parent?: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + parent?: ObjectTypeDeclaration; name: PropertyName; } @@ -1045,7 +1045,7 @@ namespace ts { export interface IndexSignatureDeclaration extends SignatureDeclarationBase, ClassElement, TypeElement { kind: SyntaxKind.IndexSignature; - parent?: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + parent?: ObjectTypeDeclaration; } export interface TypeNode extends Node { @@ -2026,6 +2026,8 @@ namespace ts { block: Block; } + export type ObjectTypeDeclaration = ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + export type DeclarationWithTypeParameters = SignatureDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTemplateTag; export interface ClassLikeDeclarationBase extends NamedDeclaration, JSDocContainer { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index c686dbcee54..bcb9f5992a9 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -948,7 +948,7 @@ namespace ts { case SyntaxKind.ClassDeclaration: case SyntaxKind.ClassExpression: case SyntaxKind.TypeLiteral: - return (node).members; + return (node).members; case SyntaxKind.ObjectLiteralExpression: return (node).properties; } @@ -3910,6 +3910,10 @@ namespace ts { seen.set(key, true); return true; } + + export function isObjectTypeDeclaration(node: Node): node is ObjectTypeDeclaration { + return isClassLike(node) || isInterfaceDeclaration(node) || isTypeLiteralNode(node); + } } namespace ts { diff --git a/src/services/completions.ts b/src/services/completions.ts index 1173b2cfd45..a51452ac879 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -18,7 +18,8 @@ namespace ts.Completions { const enum KeywordCompletionFilters { None, - ClassElementKeywords, // Keywords at class keyword + ClassElementKeywords, // Keywords inside class body + InterfaceElementKeywords, // Keywords inside interface body ConstructorParameterKeywords, // Keywords at constructor parameter FunctionLikeBodyKeywords, // Keywords at function like body TypeKeywords, @@ -1527,58 +1528,51 @@ namespace ts.Completions { * Relevant symbols are stored in the captured 'symbols' variable. */ function tryGetClassLikeCompletionSymbols(): GlobalsSearch { - const classLikeDeclaration = tryGetClassLikeCompletionContainer(contextToken); - if (!classLikeDeclaration) return GlobalsSearch.Continue; + const decl = tryGetObjectTypeDeclarationCompletionContainer(sourceFile, contextToken, location); + if (!decl) return GlobalsSearch.Continue; // We're looking up possible property names from parent type. completionKind = CompletionKind.MemberLike; // Declaring new property/method/accessor isNewIdentifierLocation = true; // Has keywords for class elements - keywordFilters = KeywordCompletionFilters.ClassElementKeywords; + keywordFilters = isClassLike(decl) ? KeywordCompletionFilters.ClassElementKeywords : KeywordCompletionFilters.InterfaceElementKeywords; - const baseTypeNode = getClassExtendsHeritageClauseElement(classLikeDeclaration); - const implementsTypeNodes = getClassImplementsHeritageClauseElements(classLikeDeclaration); - if (baseTypeNode || implementsTypeNodes) { - const classElement = contextToken.parent; - let classElementModifierFlags = isClassElement(classElement) && getModifierFlags(classElement); + // If you're in an interface you don't want to repeat things from super-interface. So just stop here. + if (!isClassLike(decl)) return GlobalsSearch.Success; + + const baseTypeNode = getClassExtendsHeritageClauseElement(decl); + const implementsTypeNodes = getClassImplementsHeritageClauseElements(decl); + if (!baseTypeNode && !implementsTypeNodes) return GlobalsSearch.Success; + + const classElement = contextToken.parent; + const classElementModifierFlags = (isClassElement(classElement) ? getModifierFlags(classElement) : ModifierFlags.None) // If this is context token is not something we are editing now, consider if this would lead to be modifier - if (contextToken.kind === SyntaxKind.Identifier && !isCurrentlyEditingNode(contextToken)) { - switch (contextToken.getText()) { - case "private": - classElementModifierFlags = classElementModifierFlags | ModifierFlags.Private; - break; - case "static": - classElementModifierFlags = classElementModifierFlags | ModifierFlags.Static; - break; - } - } + | (isIdentifier(contextToken) && !isCurrentlyEditingNode(contextToken) ? modifierToFlag(contextToken.originalKeywordKind) : ModifierFlags.None); - // No member list for private methods - if (!(classElementModifierFlags & ModifierFlags.Private)) { - let baseClassTypeToGetPropertiesFrom: Type; - if (baseTypeNode) { - baseClassTypeToGetPropertiesFrom = typeChecker.getTypeAtLocation(baseTypeNode); - if (classElementModifierFlags & ModifierFlags.Static) { - // Use static class to get property symbols from - baseClassTypeToGetPropertiesFrom = typeChecker.getTypeOfSymbolAtLocation( - baseClassTypeToGetPropertiesFrom.symbol, classLikeDeclaration); - } - } - const implementedInterfaceTypePropertySymbols = (classElementModifierFlags & ModifierFlags.Static) ? - emptyArray : - flatMap(implementsTypeNodes || emptyArray, typeNode => typeChecker.getPropertiesOfType(typeChecker.getTypeAtLocation(typeNode))); + // No member list for private methods + if (classElementModifierFlags & ModifierFlags.Private) return GlobalsSearch.Success; - // List of property symbols of base type that are not private and already implemented - symbols = filterClassMembersList( - baseClassTypeToGetPropertiesFrom ? - typeChecker.getPropertiesOfType(baseClassTypeToGetPropertiesFrom) : - emptyArray, - implementedInterfaceTypePropertySymbols, - classLikeDeclaration.members, - classElementModifierFlags); + let baseClassTypeToGetPropertiesFrom: Type | undefined; + if (baseTypeNode) { + baseClassTypeToGetPropertiesFrom = typeChecker.getTypeAtLocation(baseTypeNode); + if (classElementModifierFlags & ModifierFlags.Static) { + // Use static class to get property symbols from + baseClassTypeToGetPropertiesFrom = typeChecker.getTypeOfSymbolAtLocation(baseClassTypeToGetPropertiesFrom.symbol, decl); } } + + const implementedInterfaceTypePropertySymbols = !implementsTypeNodes || (classElementModifierFlags & ModifierFlags.Static) + ? emptyArray + : flatMap(implementsTypeNodes, typeNode => typeChecker.getPropertiesOfType(typeChecker.getTypeAtLocation(typeNode))); + + // List of property symbols of base type that are not private and already implemented + symbols = filterClassMembersList( + baseClassTypeToGetPropertiesFrom ? typeChecker.getPropertiesOfType(baseClassTypeToGetPropertiesFrom) : emptyArray, + implementedInterfaceTypePropertySymbols, + decl.members, + classElementModifierFlags); + return GlobalsSearch.Success; } @@ -1622,10 +1616,6 @@ namespace ts.Completions { return undefined; } - function isFromClassElementDeclaration(node: Node) { - return node.parent && isClassElement(node.parent) && isClassLike(node.parent.parent); - } - function isParameterOfConstructorDeclaration(node: Node) { return isParameter(node) && isConstructorDeclaration(node.parent); } @@ -1636,56 +1626,6 @@ namespace ts.Completions { (isConstructorParameterCompletionKeyword(node.kind) || isDeclarationName(node)); } - /** - * Returns the immediate owning class declaration of a context token, - * on the condition that one exists and that the context implies completion should be given. - */ - function tryGetClassLikeCompletionContainer(contextToken: Node): ClassLikeDeclaration { - if (contextToken) { - switch (contextToken.kind) { - case SyntaxKind.OpenBraceToken: // class c { | - if (isClassLike(contextToken.parent)) { - return contextToken.parent; - } - break; - - // class c {getValue(): number, | } - case SyntaxKind.CommaToken: - if (isClassLike(contextToken.parent)) { - return contextToken.parent; - } - break; - - // class c {getValue(): number; | } - case SyntaxKind.SemicolonToken: - // class c { method() { } | } - case SyntaxKind.CloseBraceToken: - if (isClassLike(location)) { - return location; - } - // class c { method() { } b| } - if (isFromClassElementDeclaration(location) && - (location.parent as ClassElement).name === location) { - return location.parent.parent as ClassLikeDeclaration; - } - break; - - default: - if (isFromClassElementDeclaration(contextToken) && - (isClassMemberCompletionKeyword(contextToken.kind) || - isClassMemberCompletionKeywordText(contextToken.getText()))) { - return contextToken.parent.parent as ClassLikeDeclaration; - } - } - } - - // class c { method() { } | method2() { } } - if (location && location.kind === SyntaxKind.SyntaxList && isClassLike(location.parent)) { - return location.parent; - } - return undefined; - } - /** * Returns the immediate owning class declaration of a context token, * on the condition that one exists and that the context implies completion should be given. @@ -1820,15 +1760,7 @@ namespace ts.Completions { isFunctionLikeButNotConstructor(containingNodeKind); case SyntaxKind.OpenBraceToken: - return containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { | - containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface a { | - containingNodeKind === SyntaxKind.TypeLiteral; // const x : { | - - case SyntaxKind.SemicolonToken: - return containingNodeKind === SyntaxKind.PropertySignature && - contextToken.parent && contextToken.parent.parent && - (contextToken.parent.parent.kind === SyntaxKind.InterfaceDeclaration || // interface a { f; | - contextToken.parent.parent.kind === SyntaxKind.TypeLiteral); // const x : { a; | + return containingNodeKind === SyntaxKind.EnumDeclaration; // enum a { | case SyntaxKind.LessThanToken: return containingNodeKind === SyntaxKind.ClassDeclaration || // class A< | @@ -1857,7 +1789,7 @@ namespace ts.Completions { case SyntaxKind.GetKeyword: case SyntaxKind.SetKeyword: - if (isFromClassElementDeclaration(contextToken)) { + if (isFromObjectTypeDeclaration(contextToken)) { return false; } // falls through @@ -1877,7 +1809,7 @@ namespace ts.Completions { // If the previous token is keyword correspoding to class member completion keyword // there will be completion available here if (isClassMemberCompletionKeywordText(contextToken.getText()) && - isFromClassElementDeclaration(contextToken)) { + isFromObjectTypeDeclaration(contextToken)) { return false; } @@ -2162,6 +2094,8 @@ namespace ts.Completions { return kind !== SyntaxKind.UndefinedKeyword; case KeywordCompletionFilters.ClassElementKeywords: return isClassMemberCompletionKeyword(kind); + case KeywordCompletionFilters.InterfaceElementKeywords: + return isInterfaceOrTypeLiteralCompletionKeyword(kind); case KeywordCompletionFilters.ConstructorParameterKeywords: return isConstructorParameterCompletionKeyword(kind); case KeywordCompletionFilters.FunctionLikeBodyKeywords: @@ -2174,6 +2108,10 @@ namespace ts.Completions { })); } + function isInterfaceOrTypeLiteralCompletionKeyword(kind: SyntaxKind): boolean { + return kind === SyntaxKind.ReadonlyKeyword; + } + function isClassMemberCompletionKeyword(kind: SyntaxKind) { switch (kind) { case SyntaxKind.PublicKeyword: @@ -2282,4 +2220,44 @@ namespace ts.Completions { !(memberType.flags & TypeFlags.Primitive || checker.isArrayLikeType(memberType) || typeHasCallOrConstructSignatures(memberType, checker))); return Debug.assertEachDefined(checker.getAllPossiblePropertiesOfTypes(filteredTypes), "getAllPossiblePropertiesOfTypes() should all be defined"); } + + /** + * Returns the immediate owning class declaration of a context token, + * on the condition that one exists and that the context implies completion should be given. + */ + function tryGetObjectTypeDeclarationCompletionContainer(sourceFile: SourceFile, contextToken: Node | undefined, location: Node): ObjectTypeDeclaration | undefined { + // class c { method() { } | method2() { } } + switch (location.kind) { + case SyntaxKind.SyntaxList: + return tryCast(location.parent, isObjectTypeDeclaration); + case SyntaxKind.EndOfFileToken: + const cls = tryCast(lastOrUndefined(cast(location.parent, isSourceFile).statements), isObjectTypeDeclaration); + if (cls && !findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile)) { + return cls; + } + } + + if (!contextToken) return undefined; + switch (contextToken.kind) { + case SyntaxKind.SemicolonToken: // class c {getValue(): number; | } + case SyntaxKind.CloseBraceToken: // class c { method() { } | } + // class c { method() { } b| } + return isFromObjectTypeDeclaration(location) && (location.parent as ClassElement | TypeElement).name === location + ? location.parent.parent as ObjectTypeDeclaration + : tryCast(location, isObjectTypeDeclaration); + case SyntaxKind.OpenBraceToken: // class c { | + case SyntaxKind.CommaToken: // class c {getValue(): number, | } + return tryCast(contextToken.parent, isObjectTypeDeclaration); + default: + if (!isFromObjectTypeDeclaration(contextToken)) return undefined; + const isValidKeyword = isClassLike(contextToken.parent.parent) ? isClassMemberCompletionKeyword : isInterfaceOrTypeLiteralCompletionKeyword; + return (isValidKeyword(contextToken.kind) || isIdentifier(contextToken) && isValidKeyword(stringToToken(contextToken.text))) + ? contextToken.parent.parent as ObjectTypeDeclaration : undefined; + } + } + + // TODO: GH#19856 Would like to return `node is Node & { parent: (ClassElement | TypeElement) & { parent: ObjectTypeDeclaration } }` but then compilation takes > 10 minutes + function isFromObjectTypeDeclaration(node: Node): boolean { + return node.parent && (isClassElement(node.parent) || isTypeElement(node.parent)) && isObjectTypeDeclaration(node.parent.parent); + } } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8c23bde1cee..c0bca578545 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -655,7 +655,7 @@ declare namespace ts { } interface MethodSignature extends SignatureDeclarationBase, TypeElement { kind: SyntaxKind.MethodSignature; - parent?: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + parent?: ObjectTypeDeclaration; name: PropertyName; } interface MethodDeclaration extends FunctionLikeDeclarationBase, ClassElement, ObjectLiteralElement, JSDocContainer { @@ -689,7 +689,7 @@ declare namespace ts { type AccessorDeclaration = GetAccessorDeclaration | SetAccessorDeclaration; interface IndexSignatureDeclaration extends SignatureDeclarationBase, ClassElement, TypeElement { kind: SyntaxKind.IndexSignature; - parent?: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + parent?: ObjectTypeDeclaration; } interface TypeNode extends Node { _typeNodeBrand: any; @@ -1266,6 +1266,7 @@ declare namespace ts { variableDeclaration?: VariableDeclaration; block: Block; } + type ObjectTypeDeclaration = ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; type DeclarationWithTypeParameters = SignatureDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTemplateTag; interface ClassLikeDeclarationBase extends NamedDeclaration, JSDocContainer { kind: SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index bc83496f1a8..ae3ad58ea33 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -655,7 +655,7 @@ declare namespace ts { } interface MethodSignature extends SignatureDeclarationBase, TypeElement { kind: SyntaxKind.MethodSignature; - parent?: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + parent?: ObjectTypeDeclaration; name: PropertyName; } interface MethodDeclaration extends FunctionLikeDeclarationBase, ClassElement, ObjectLiteralElement, JSDocContainer { @@ -689,7 +689,7 @@ declare namespace ts { type AccessorDeclaration = GetAccessorDeclaration | SetAccessorDeclaration; interface IndexSignatureDeclaration extends SignatureDeclarationBase, ClassElement, TypeElement { kind: SyntaxKind.IndexSignature; - parent?: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + parent?: ObjectTypeDeclaration; } interface TypeNode extends Node { _typeNodeBrand: any; @@ -1266,6 +1266,7 @@ declare namespace ts { variableDeclaration?: VariableDeclaration; block: Block; } + type ObjectTypeDeclaration = ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; type DeclarationWithTypeParameters = SignatureDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTemplateTag; interface ClassLikeDeclarationBase extends NamedDeclaration, JSDocContainer { kind: SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression; diff --git a/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers.ts b/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers.ts deleted file mode 100644 index 9998f38c255..00000000000 --- a/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -////var aa = 1; - -////interface a { /*interfaceValue1*/ - -goTo.eachMarker(() => verify.completionListIsEmpty()); diff --git a/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers2.ts b/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers2.ts deleted file mode 100644 index 234e41bfb71..00000000000 --- a/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers2.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -////var aa = 1; - -////interface a { f/*interfaceValue2*/ - -goTo.eachMarker(() => verify.completionListIsEmpty()); diff --git a/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers3.ts b/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers3.ts deleted file mode 100644 index f0a596ef8b1..00000000000 --- a/tests/cases/fourslash/completionListAtIdentifierDefinitionLocations_interfaceMembers3.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -////var aa = 1; - -////interface a { f; /*interfaceValue3*/ - -goTo.eachMarker(() => verify.completionListIsEmpty()); diff --git a/tests/cases/fourslash/completionListInUnclosedObjectTypeLiteralInSignature04.ts b/tests/cases/fourslash/completionListInUnclosedObjectTypeLiteralInSignature04.ts index 2c435890ebd..d6746803ab8 100644 --- a/tests/cases/fourslash/completionListInUnclosedObjectTypeLiteralInSignature04.ts +++ b/tests/cases/fourslash/completionListInUnclosedObjectTypeLiteralInSignature04.ts @@ -7,10 +7,4 @@ //// ////declare function foo(obj: I): { /*1*/ -goTo.marker("1"); - -verify.not.completionListContains("I"); -verify.not.completionListContains("TString"); -verify.not.completionListContains("TNumber"); -verify.not.completionListContains("foo"); -verify.not.completionListContains("obj"); +verify.completionsAt("1", ["readonly"]); diff --git a/tests/cases/fourslash/completionListWithModulesInsideModuleScope.ts b/tests/cases/fourslash/completionListWithModulesInsideModuleScope.ts index cb6bf283b69..d13141c38cb 100644 --- a/tests/cases/fourslash/completionListWithModulesInsideModuleScope.ts +++ b/tests/cases/fourslash/completionListWithModulesInsideModuleScope.ts @@ -311,7 +311,7 @@ goToMarkAndGeneralVerify('class', { isClassScope: true }); //verify.not.completionListContains('ceVar'); // from interface in mod1 -goToMarkAndGeneralVerify('interface', { insideMod1: true }); +verify.completionsAt("interface", ["readonly"]); // from namespace in mod1 verifyNamespaceInMod1('namespace'); @@ -348,7 +348,7 @@ verify.not.completionListContains('ceFunc'); verify.not.completionListContains('ceVar'); // from exported interface in mod1 -goToMarkAndGeneralVerify('exportedInterface', { insideMod1: true }); +verify.completionsAt("exportedInterface", ["readonly"]); // from exported namespace in mod1 verifyExportedNamespace('exportedNamespace'); diff --git a/tests/cases/fourslash/completionsInterfaceElement.ts b/tests/cases/fourslash/completionsInterfaceElement.ts new file mode 100644 index 00000000000..fe7d55db794 --- /dev/null +++ b/tests/cases/fourslash/completionsInterfaceElement.ts @@ -0,0 +1,18 @@ +/// + +////const foo = 0; +////interface I { +//// m(): void; +//// fo/*i*/ +////} +////interface J { /*j*/ } +////interface K { f; /*k*/ } + +////type T = { fo/*t*/ }; +////type U = { /*u*/ }; + +////interface EndOfFile { f; /*e*/ + +for (const marker of test.markerNames()) { + verify.completionsAt(marker, ["readonly"]); +}