diff --git a/src/services/completions.ts b/src/services/completions.ts index da59dab8828..cb4abb89b58 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -939,10 +939,31 @@ namespace ts.Completions { const baseTypeNode = getClassExtendsHeritageClauseElement(classLikeDeclaration); if (baseTypeNode) { - const baseType = typeChecker.getTypeAtLocation(baseTypeNode); - // List of property symbols of base type that are not private - symbols = filter(typeChecker.getPropertiesOfType(baseType), - baseProperty => !(getDeclarationModifierFlagsFromSymbol(baseProperty) & ModifierFlags.Private)); + const classElement = contextToken.parent; + let classElementModifierFlags = isClassElement(classElement) && getModifierFlags(classElement); + // 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; + } + } + + // No member list for private methods + if (!(classElementModifierFlags & ModifierFlags.Private)) { + const baseType = typeChecker.getTypeAtLocation(baseTypeNode); + const typeToGetPropertiesFrom = (classElementModifierFlags & ModifierFlags.Static) ? + typeChecker.getTypeOfSymbolAtLocation(baseType.symbol, classLikeDeclaration) : + baseType; + + // List of property symbols of base type that are not private + symbols = filter(typeChecker.getPropertiesOfType(typeToGetPropertiesFrom), + baseProperty => baseProperty.getDeclarations() && !(getDeclarationModifierFlagsFromSymbol(baseProperty) & ModifierFlags.Private)); + } } return true; @@ -1248,7 +1269,7 @@ namespace ts.Completions { for (const element of namedImportsOrExports) { // If this is the current item we are editing right now, do not filter it out - if (element.getStart() <= position && position <= element.getEnd()) { + if (isCurrentlyEditingNode(element)) { continue; } @@ -1287,7 +1308,7 @@ namespace ts.Completions { } // If this is the current item we are editing right now, do not filter it out - if (m.getStart() <= position && position <= m.getEnd()) { + if (isCurrentlyEditingNode(m)) { continue; } @@ -1322,7 +1343,7 @@ namespace ts.Completions { const seenNames = createMap(); for (const attr of attributes) { // If this is the current item we are editing right now, do not filter it out - if (attr.getStart() <= position && position <= attr.getEnd()) { + if (isCurrentlyEditingNode(attr)) { continue; } @@ -1333,6 +1354,10 @@ namespace ts.Completions { return filter(symbols, a => !seenNames.get(a.name)); } + + function isCurrentlyEditingNode(node: Node): boolean { + return node.getStart() <= position && position <= node.getEnd(); + } } /** diff --git a/tests/cases/fourslash/completionEntryForClassMembers.ts b/tests/cases/fourslash/completionEntryForClassMembers.ts index c52977a0002..672046f62a7 100644 --- a/tests/cases/fourslash/completionEntryForClassMembers.ts +++ b/tests/cases/fourslash/completionEntryForClassMembers.ts @@ -23,12 +23,19 @@ ////class F extends B { //// public /*classThatHasWrittenPublicKeyword*/ ////} +////class F2 extends B { +//// private /*classThatHasWrittenPrivateKeyword*/ +////} ////class G extends B { //// static /*classElementContainingStatic*/ ////} +////class G2 extends B { +//// private static /*classElementContainingPrivateStatic*/ +////} ////class H extends B { //// prop/*classThatStartedWritingIdentifier*/ ////} +//////Class for location verification ////class I extends B { //// prop0: number //// /*propDeclarationWithoutSemicolon*/ @@ -68,38 +75,87 @@ ////class L extends B { //// public identi/*classThatStartedWritingIdentifierAfterModifier*/ ////} -////class L extends B { +////class L2 extends B { +//// private identi/*classThatStartedWritingIdentifierAfterPrivateModifier*/ +////} +////class M extends B { //// static identi/*classThatStartedWritingIdentifierAfterStaticModifier*/ ////} +////class M extends B { +//// private static identi/*classThatStartedWritingIdentifierAfterPrivateStaticModifier*/ +////} const allowedKeywordCount = verify.allowedClassElementKeywords.length; +type CompletionInfo = [string, string]; +type CompletionInfoVerifier = { validMembers: CompletionInfo[], invalidMembers: CompletionInfo[] }; + +function verifyClassElementLocations({ validMembers, invalidMembers }: CompletionInfoVerifier, classElementCompletionLocations: string[]) { + for (const marker of classElementCompletionLocations) { + goTo.marker(marker); + verifyCompletionInfo(validMembers, verify); + verifyCompletionInfo(invalidMembers, verify.not); + verify.completionListContainsClassElementKeywords(); + verify.completionListCount(allowedKeywordCount + validMembers.length); + } +} + +function verifyCompletionInfo(memberInfo: CompletionInfo[], verify: FourSlashInterface.verifyNegatable) { + for (const [symbol, text] of memberInfo) { + verify.completionListContains(symbol, text, /*documentation*/ undefined, "method"); + } +} + +const allMembersOfBase: CompletionInfo[] = [ + ["getValue", "(method) B.getValue(): number"], + ["protectedMethod", "(method) B.protectedMethod(): void"], + ["privateMethod", "(method) B.privateMethod(): void"], + ["staticMethod", "(method) B.staticMethod(): void"] +]; +function filterCompletionInfo(fn: (a: CompletionInfo) => boolean): CompletionInfoVerifier { + const validMembers: CompletionInfo[] = []; + const invalidMembers: CompletionInfo[] = []; + for (const member of allMembersOfBase) { + if (fn(member)) { + validMembers.push(member); + } + else { + invalidMembers.push(member); + } + } + return { validMembers, invalidMembers }; +} + + +const instanceMemberInfo = filterCompletionInfo(([a]: CompletionInfo) => a === "getValue" || a === "protectedMethod"); +const staticMemberInfo = filterCompletionInfo(([a]: CompletionInfo) => a === "staticMethod"); + +// Not a class element declaration location const nonClassElementMarkers = [ "InsideMethod" ]; for (const marker of nonClassElementMarkers) { goTo.marker(marker); - verify.not.completionListContains("getValue"); + verifyCompletionInfo(allMembersOfBase, verify.not); verify.not.completionListIsEmpty(); } -// Only keywords allowed at this position since they dont extend the class +// Only keywords allowed at this position since they dont extend the class or are private const onlyClassElementKeywordLocations = [ "abstractClass", - "classThatDoesNotExtendAnotherClass" + "classThatDoesNotExtendAnotherClass", + "classThatHasWrittenPrivateKeyword", + "classElementContainingPrivateStatic", + "classThatStartedWritingIdentifierAfterPrivateModifier", + "classThatStartedWritingIdentifierAfterPrivateStaticModifier" ]; -for (const marker of onlyClassElementKeywordLocations) { - goTo.marker(marker); - verify.completionListContainsClassElementKeywords(); - verify.completionListCount(allowedKeywordCount); -} +verifyClassElementLocations({ validMembers: [], invalidMembers: allMembersOfBase }, onlyClassElementKeywordLocations); -// Base members and class member keywords allowed -const classElementCompletionLocations = [ +// Instance base members and class member keywords allowed +const classInstanceElementLocations = [ "classThatIsEmptyAndExtendingAnotherClass", "classThatHasAlreadyImplementedAnotherClassMethod", "classThatHasAlreadyImplementedAnotherClassMethodAfterMethod", "classThatHasWrittenPublicKeyword", - "classElementContainingStatic", "classThatStartedWritingIdentifier", "propDeclarationWithoutSemicolon", "propDeclarationWithSemicolon", @@ -115,25 +171,12 @@ const classElementCompletionLocations = [ "classThatStartedWritingIdentifierOfGetAccessor", "classThatStartedWritingIdentifierOfSetAccessor", "classThatStartedWritingIdentifierAfterModifier", +]; +verifyClassElementLocations(instanceMemberInfo, classInstanceElementLocations); + +// Static Base members and class member keywords allowed +const staticClassLocations = [ + "classElementContainingStatic", "classThatStartedWritingIdentifierAfterStaticModifier" ]; - -const validMembersOfBase = [ - ["getValue", "(method) B.getValue(): number"], - ["protectedMethod", "(method) B.protectedMethod(): void"] -]; -const invalidMembersOfBase = [ - ["privateMethod", "(method) B.privateMethod(): void"], - ["staticMethod", "(method) B.staticMethod(): void"] -]; -for (const marker of classElementCompletionLocations) { - goTo.marker(marker); - for (const [validMemberOfBaseSymbol, validMemberOfBaseText] of validMembersOfBase) { - verify.completionListContains(validMemberOfBaseSymbol, validMemberOfBaseText, /*documentation*/ undefined, "method"); - } - for (const [invalidMemberOfBaseSymbol, invalidMemberOfBaseText] of invalidMembersOfBase) { - verify.not.completionListContains(invalidMemberOfBaseSymbol, invalidMemberOfBaseText, /*documentation*/ undefined, "method"); - } - verify.completionListContainsClassElementKeywords(); - verify.completionListCount(allowedKeywordCount + validMembersOfBase.length); -} \ No newline at end of file +verifyClassElementLocations(staticMemberInfo, staticClassLocations); \ No newline at end of file