Skip to content

Commit 945f675

Browse files
committed
Completion list for a class extending another class should contain members from base class
Handles #7158
1 parent 1db4f96 commit 945f675

9 files changed

+313
-57
lines changed

src/compiler/checker.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -679,10 +679,6 @@ namespace ts {
679679
return type.flags & TypeFlags.Object ? (<ObjectType>type).objectFlags : 0;
680680
}
681681

682-
function getCheckFlags(symbol: Symbol): CheckFlags {
683-
return symbol.flags & SymbolFlags.Transient ? (<TransientSymbol>symbol).checkFlags : 0;
684-
}
685-
686682
function isGlobalSourceFile(node: Node) {
687683
return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(<SourceFile>node);
688684
}
@@ -13914,25 +13910,6 @@ namespace ts {
1391413910
return s.valueDeclaration ? s.valueDeclaration.kind : SyntaxKind.PropertyDeclaration;
1391513911
}
1391613912

13917-
function getDeclarationModifierFlagsFromSymbol(s: Symbol): ModifierFlags {
13918-
if (s.valueDeclaration) {
13919-
const flags = getCombinedModifierFlags(s.valueDeclaration);
13920-
return s.parent && s.parent.flags & SymbolFlags.Class ? flags : flags & ~ModifierFlags.AccessibilityModifier;
13921-
}
13922-
if (getCheckFlags(s) & CheckFlags.Synthetic) {
13923-
const checkFlags = (<TransientSymbol>s).checkFlags;
13924-
const accessModifier = checkFlags & CheckFlags.ContainsPrivate ? ModifierFlags.Private :
13925-
checkFlags & CheckFlags.ContainsPublic ? ModifierFlags.Public :
13926-
ModifierFlags.Protected;
13927-
const staticModifier = checkFlags & CheckFlags.ContainsStatic ? ModifierFlags.Static : 0;
13928-
return accessModifier | staticModifier;
13929-
}
13930-
if (s.flags & SymbolFlags.Prototype) {
13931-
return ModifierFlags.Public | ModifierFlags.Static;
13932-
}
13933-
return 0;
13934-
}
13935-
1393613913
function getDeclarationNodeFlagsFromSymbol(s: Symbol): NodeFlags {
1393713914
return s.valueDeclaration ? getCombinedNodeFlags(s.valueDeclaration) : 0;
1393813915
}

src/compiler/utilities.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4198,6 +4198,29 @@ namespace ts {
41984198
// Firefox has Object.prototype.watch
41994199
return options.watch && options.hasOwnProperty("watch");
42004200
}
4201+
4202+
export function getCheckFlags(symbol: Symbol): CheckFlags {
4203+
return symbol.flags & SymbolFlags.Transient ? (<TransientSymbol>symbol).checkFlags : 0;
4204+
}
4205+
4206+
export function getDeclarationModifierFlagsFromSymbol(s: Symbol): ModifierFlags {
4207+
if (s.valueDeclaration) {
4208+
const flags = getCombinedModifierFlags(s.valueDeclaration);
4209+
return s.parent && s.parent.flags & SymbolFlags.Class ? flags : flags & ~ModifierFlags.AccessibilityModifier;
4210+
}
4211+
if (getCheckFlags(s) & CheckFlags.Synthetic) {
4212+
const checkFlags = (<TransientSymbol>s).checkFlags;
4213+
const accessModifier = checkFlags & CheckFlags.ContainsPrivate ? ModifierFlags.Private :
4214+
checkFlags & CheckFlags.ContainsPublic ? ModifierFlags.Public :
4215+
ModifierFlags.Protected;
4216+
const staticModifier = checkFlags & CheckFlags.ContainsStatic ? ModifierFlags.Static : 0;
4217+
return accessModifier | staticModifier;
4218+
}
4219+
if (s.flags & SymbolFlags.Prototype) {
4220+
return ModifierFlags.Public | ModifierFlags.Static;
4221+
}
4222+
return 0;
4223+
}
42014224
}
42024225

42034226
namespace ts {

src/services/completions.ts

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace ts.Completions {
1818
return undefined;
1919
}
2020

21-
const { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, requestJsDocTagName, requestJsDocTag } = completionData;
21+
const { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords } = completionData;
2222

2323
if (requestJsDocTagName) {
2424
// If the current position is a jsDoc tag name, only tag names should be provided for completion
@@ -52,16 +52,19 @@ namespace ts.Completions {
5252
sortText: "0",
5353
});
5454
}
55-
else {
55+
else if (!hasFilteredClassMemberKeywords) {
5656
return undefined;
5757
}
5858
}
5959

6060
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
6161
}
6262

63+
if (hasFilteredClassMemberKeywords) {
64+
addRange(entries, classMemberKeywordCompletions);
65+
}
6366
// Add keywords if this is not a member completion list
64-
if (!isMemberCompletion && !requestJsDocTag && !requestJsDocTagName) {
67+
else if (!isMemberCompletion && !requestJsDocTag && !requestJsDocTagName) {
6568
addRange(entries, keywordCompletions);
6669
}
6770

@@ -406,7 +409,7 @@ namespace ts.Completions {
406409
}
407410

408411
if (requestJsDocTagName || requestJsDocTag) {
409-
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, requestJsDocTagName, requestJsDocTag };
412+
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords: false };
410413
}
411414

412415
if (!insideJsDocTagExpression) {
@@ -505,6 +508,7 @@ namespace ts.Completions {
505508
let isGlobalCompletion = false;
506509
let isMemberCompletion: boolean;
507510
let isNewIdentifierLocation: boolean;
511+
let hasFilteredClassMemberKeywords = false;
508512
let symbols: Symbol[] = [];
509513

510514
if (isRightOfDot) {
@@ -542,7 +546,7 @@ namespace ts.Completions {
542546

543547
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
544548

545-
return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), requestJsDocTagName, requestJsDocTag };
549+
return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords };
546550

547551
function getTypeScriptMemberSymbols(): void {
548552
// Right of dot member completion list
@@ -599,6 +603,7 @@ namespace ts.Completions {
599603
function tryGetGlobalSymbols(): boolean {
600604
let objectLikeContainer: ObjectLiteralExpression | BindingPattern;
601605
let namedImportsOrExports: NamedImportsOrExports;
606+
let classLikeContainer: ClassLikeDeclaration;
602607
let jsxContainer: JsxOpeningLikeElement;
603608

604609
if (objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken)) {
@@ -611,6 +616,11 @@ namespace ts.Completions {
611616
return tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports);
612617
}
613618

619+
if (classLikeContainer = tryGetClassLikeCompletionContainer(contextToken)) {
620+
// cursor inside class declaration
621+
return tryGetClassLikeCompletionSymbols(classLikeContainer);
622+
}
623+
614624
if (jsxContainer = tryGetContainingJsxElement(contextToken)) {
615625
let attrsType: Type;
616626
if ((jsxContainer.kind === SyntaxKind.JsxSelfClosingElement) || (jsxContainer.kind === SyntaxKind.JsxOpeningElement)) {
@@ -913,6 +923,31 @@ namespace ts.Completions {
913923
return true;
914924
}
915925

926+
/**
927+
* Aggregates relevant symbols for completion in class declaration
928+
* Relevant symbols are stored in the captured 'symbols' variable.
929+
*
930+
* @returns true if 'symbols' was successfully populated; false otherwise.
931+
*/
932+
function tryGetClassLikeCompletionSymbols(classLikeDeclaration: ClassLikeDeclaration): boolean {
933+
// We're looking up possible property names from parent type.
934+
isMemberCompletion = true;
935+
// Declaring new property/method/accessor
936+
isNewIdentifierLocation = true;
937+
// Has keywords for class elements
938+
hasFilteredClassMemberKeywords = true;
939+
940+
const baseTypeNode = getClassExtendsHeritageClauseElement(classLikeDeclaration);
941+
if (baseTypeNode) {
942+
const baseType = typeChecker.getTypeAtLocation(baseTypeNode);
943+
// List of property symbols of base type that are not private
944+
symbols = filter(typeChecker.getPropertiesOfType(baseType),
945+
baseProperty => !(getDeclarationModifierFlagsFromSymbol(baseProperty) & ModifierFlags.Private));
946+
}
947+
948+
return true;
949+
}
950+
916951
/**
917952
* Returns the immediate owning object literal or binding pattern of a context token,
918953
* on the condition that one exists and that the context implies completion should be given.
@@ -953,6 +988,38 @@ namespace ts.Completions {
953988
return undefined;
954989
}
955990

991+
/**
992+
* Returns the immediate owning class declaration of a context token,
993+
* on the condition that one exists and that the context implies completion should be given.
994+
*/
995+
function tryGetClassLikeCompletionContainer(contextToken: Node): ClassLikeDeclaration {
996+
if (contextToken) {
997+
switch (contextToken.kind) {
998+
case SyntaxKind.OpenBraceToken: // class c { |
999+
if (isClassLike(contextToken.parent)) {
1000+
return contextToken.parent;
1001+
}
1002+
break;
1003+
1004+
// class c {getValue(): number; | }
1005+
case SyntaxKind.CommaToken:
1006+
case SyntaxKind.SemicolonToken:
1007+
// class c { method() { } | }
1008+
case SyntaxKind.CloseBraceToken:
1009+
if (isClassLike(location)) {
1010+
return location;
1011+
}
1012+
break;
1013+
}
1014+
}
1015+
1016+
// class c { method() { } | method2() { } }
1017+
if (location && location.kind === SyntaxKind.SyntaxList && isClassLike(location.parent)) {
1018+
return location.parent;
1019+
}
1020+
return undefined;
1021+
}
1022+
9561023
function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement {
9571024
if (contextToken) {
9581025
const parent = contextToken.parent;
@@ -1306,6 +1373,21 @@ namespace ts.Completions {
13061373
});
13071374
}
13081375

1376+
const classMemberKeywordCompletions = filter(keywordCompletions, entry => {
1377+
switch (stringToToken(entry.name)) {
1378+
case SyntaxKind.PublicKeyword:
1379+
case SyntaxKind.ProtectedKeyword:
1380+
case SyntaxKind.PrivateKeyword:
1381+
case SyntaxKind.AbstractKeyword:
1382+
case SyntaxKind.StaticKeyword:
1383+
case SyntaxKind.ConstructorKeyword:
1384+
case SyntaxKind.ReadonlyKeyword:
1385+
case SyntaxKind.GetKeyword:
1386+
case SyntaxKind.SetKeyword:
1387+
return true;
1388+
}
1389+
});
1390+
13091391
function isEqualityExpression(node: Node): node is BinaryExpression {
13101392
return isBinaryExpression(node) && isEqualityOperatorKind(node.operatorToken.kind);
13111393
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
///<reference path="fourslash.ts" />
2+
3+
////abstract class B {
4+
//// abstract getValue(): number;
5+
//// /*abstractClass*/
6+
////}
7+
////class C extends B {
8+
//// /*classThatIsEmptyAndExtendingAnotherClass*/
9+
////}
10+
////class D extends B {
11+
//// /*classThatHasAlreadyImplementedAnotherClassMethod*/
12+
//// getValue() {
13+
//// return 10;
14+
//// }
15+
//// /*classThatHasAlreadyImplementedAnotherClassMethodAfterMethod*/
16+
////}
17+
////class E {
18+
//// /*classThatDoesNotExtendAnotherClass*/
19+
////}
20+
////class F extends B {
21+
//// public /*classThatHasWrittenPublicKeyword*/
22+
////}
23+
////class G extends B {
24+
//// static /*classElementContainingStatic*/
25+
////}
26+
////class H extends B {
27+
//// prop/*classThatStartedWritingIdentifier*/
28+
////}
29+
////class I extends B {
30+
//// prop0: number
31+
//// /*propDeclarationWithoutSemicolon*/
32+
//// prop: number;
33+
//// /*propDeclarationWithSemicolon*/
34+
//// prop1 = 10;
35+
//// /*propAssignmentWithSemicolon*/
36+
//// prop2 = 10
37+
//// /*propAssignmentWithoutSemicolon*/
38+
//// method(): number
39+
//// /*methodSignatureWithoutSemicolon*/
40+
//// method2(): number;
41+
//// /*methodSignatureWithSemicolon*/
42+
//// method3() {
43+
//// /*InsideMethod*/
44+
//// }
45+
//// /*methodImplementation*/
46+
//// get c()
47+
//// /*accessorSignatureWithoutSemicolon*/
48+
//// set c()
49+
//// {
50+
//// }
51+
//// /*accessorSignatureImplementation*/
52+
////}
53+
////class J extends B {
54+
//// get /*classThatHasWrittenGetKeyword*/
55+
////}
56+
////class K extends B {
57+
//// set /*classThatHasWrittenSetKeyword*/
58+
////}
59+
////class J extends B {
60+
//// get identi/*classThatStartedWritingIdentifierOfGetAccessor*/
61+
////}
62+
////class K extends B {
63+
//// set identi/*classThatStartedWritingIdentifierOfSetAccessor*/
64+
////}
65+
////class L extends B {
66+
//// public identi/*classThatStartedWritingIdentifierAfterModifier*/
67+
////}
68+
////class L extends B {
69+
//// static identi/*classThatStartedWritingIdentifierAfterStaticModifier*/
70+
////}
71+
72+
const allowedKeywords = [
73+
"public",
74+
"private",
75+
"protected",
76+
"static",
77+
"abstract",
78+
"readonly",
79+
"get",
80+
"set",
81+
"constructor"
82+
];
83+
84+
const allowedKeywordCount = allowedKeywords.length;
85+
function verifyAllowedKeyWords() {
86+
for (const keyword of allowedKeywords) {
87+
verify.completionListContains(keyword, keyword, /*documentation*/ undefined, "keyword");
88+
}
89+
}
90+
91+
const nonClassElementMarkers = [
92+
"InsideMethod"
93+
];
94+
for (const marker of nonClassElementMarkers) {
95+
goTo.marker(marker);
96+
verify.not.completionListContains("getValue");
97+
verify.not.completionListIsEmpty();
98+
}
99+
100+
// Only keywords allowed at this position since they dont extend the class
101+
const onlyClassElementKeywordLocations = [
102+
"abstractClass",
103+
"classThatDoesNotExtendAnotherClass"
104+
];
105+
for (const marker of onlyClassElementKeywordLocations) {
106+
goTo.marker(marker);
107+
verifyAllowedKeyWords();
108+
verify.completionListCount(allowedKeywordCount);
109+
}
110+
111+
// Base members and class member keywords allowed
112+
const classElementCompletionLocations = [
113+
"classThatIsEmptyAndExtendingAnotherClass",
114+
"classThatHasAlreadyImplementedAnotherClassMethod",
115+
"classThatHasAlreadyImplementedAnotherClassMethodAfterMethod",
116+
// TODO should we give completion for these keywords
117+
//"classThatHasWrittenPublicKeyword",
118+
//"classElementContainingStatic",
119+
"classThatStartedWritingIdentifier",
120+
"propDeclarationWithoutSemicolon",
121+
"propDeclarationWithSemicolon",
122+
"propAssignmentWithSemicolon",
123+
"propAssignmentWithoutSemicolon",
124+
"methodSignatureWithoutSemicolon",
125+
"methodSignatureWithSemicolon",
126+
"methodImplementation",
127+
"accessorSignatureWithoutSemicolon",
128+
"accessorSignatureImplementation",
129+
// TODO should we give completion for these keywords
130+
//"classThatHasWrittenGetKeyword",
131+
//"classThatHasWrittenSetKeyword",
132+
//"classThatStartedWritingIdentifierOfGetAccessor",
133+
//"classThatStartedWritingIdentifierOfSetAccessor",
134+
//"classThatStartedWritingIdentifierAfterModifier",
135+
//"classThatStartedWritingIdentifierAfterStaticModifier"
136+
];
137+
for (const marker of classElementCompletionLocations) {
138+
goTo.marker(marker);
139+
verify.completionListContains("getValue", "(method) B.getValue(): number", /*documentation*/ undefined, "method");
140+
verifyAllowedKeyWords();
141+
verify.completionListCount(allowedKeywordCount + 1);
142+
}

tests/cases/fourslash/completionListInNamedClassExpression.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
///<reference path="fourslash.ts" />
1+
///<reference path="fourslash.ts" />
22

33
//// var x = class myClass {
44
//// getClassName (){
@@ -11,4 +11,4 @@ goTo.marker("0");
1111
verify.completionListContains("myClass", "(local class) myClass", /*documentation*/ undefined, "local class");
1212

1313
goTo.marker("1");
14-
verify.completionListContains("myClass", "(local class) myClass", /*documentation*/ undefined, "local class");
14+
verify.not.completionListContains("myClass", "(local class) myClass", /*documentation*/ undefined, "local class");

0 commit comments

Comments
 (0)