Skip to content

Commit 9550efd

Browse files
author
Andy Hanson
committed
Add completions from the 'this' type
1 parent 64b3086 commit 9550efd

File tree

7 files changed

+90
-21
lines changed

7 files changed

+90
-21
lines changed

src/compiler/checker.ts

+20-9
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,15 @@ namespace ts {
295295
getAccessibleSymbolChain,
296296
getTypePredicateOfSignature,
297297
resolveExternalModuleSymbol,
298+
tryGetThisTypeAt: node => {
299+
node = getParseTreeNode(node);
300+
return node && tryGetThisTypeAt(node);
301+
},
302+
isMemberSymbol: symbol =>
303+
symbol.flags & SymbolFlags.ClassMember
304+
&& symbol !== argumentsSymbol
305+
&& symbol !== undefinedSymbol
306+
&& !(symbol.parent && symbol.parent.flags & SymbolFlags.Module),
298307
};
299308

300309
const tupleTypes: GenericType[] = [];
@@ -13268,6 +13277,16 @@ namespace ts {
1326813277
if (needToCaptureLexicalThis) {
1326913278
captureLexicalThis(node, container);
1327013279
}
13280+
13281+
const type = tryGetThisTypeAt(node, container);
13282+
if (!type && noImplicitThis) {
13283+
// With noImplicitThis, functions may not reference 'this' if it has type 'any'
13284+
error(node, Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation);
13285+
}
13286+
return type || anyType;
13287+
}
13288+
13289+
function tryGetThisTypeAt(node: Node, container = getThisContainer(node, /*includeArrowFunctions*/ false)): Type | undefined {
1327113290
if (isFunctionLike(container) &&
1327213291
(!isInParameterInitializerBeforeContainingFunction(node) || getThisParameter(container))) {
1327313292
// Note: a parameter initializer should refer to class-this unless function-this is explicitly annotated.
@@ -13302,16 +13321,8 @@ namespace ts {
1330213321

1330313322
if (isInJavaScriptFile(node)) {
1330413323
const type = getTypeForThisExpressionFromJSDoc(container);
13305-
if (type && type !== unknownType) {
13306-
return type;
13307-
}
13324+
return type !== unknownType ? type : undefined;
1330813325
}
13309-
13310-
if (noImplicitThis) {
13311-
// With noImplicitThis, functions may not reference 'this' if it has type 'any'
13312-
error(node, Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation);
13313-
}
13314-
return anyType;
1331513326
}
1331613327

1331713328
function getTypeForThisExpressionFromJSDoc(node: Node) {

src/compiler/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -2919,6 +2919,9 @@ namespace ts {
29192919
/* @internal */ getAccessibleSymbolChain(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean): Symbol[] | undefined;
29202920
/* @internal */ getTypePredicateOfSignature(signature: Signature): TypePredicate;
29212921
/* @internal */ resolveExternalModuleSymbol(symbol: Symbol): Symbol;
2922+
/** @param node A location where we might consider accessing `this`. Not necessarily a ThisExpression. */
2923+
/* @internal */ tryGetThisTypeAt(node: Node): Type | undefined;
2924+
/* @internal */ isMemberSymbol(symbol: Symbol): boolean;
29222925
}
29232926

29242927
/* @internal */

src/harness/fourslash.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3152,8 +3152,9 @@ Actual: ${stringify(fullActual)}`);
31523152
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + entryId));
31533153
}
31543154

3155-
assert.equal(item.hasAction, hasAction);
3155+
assert.equal(item.hasAction, hasAction, "hasAction");
31563156
assert.equal(item.isRecommended, options && options.isRecommended, "isRecommended");
3157+
assert.equal(item.insertText, options && options.insertText, "insertText");
31573158
}
31583159

31593160
private findFile(indexOrName: string | number) {
@@ -4615,6 +4616,7 @@ namespace FourSlashInterface {
46154616
export interface VerifyCompletionListContainsOptions extends ts.GetCompletionsAtPositionOptions {
46164617
sourceDisplay: string;
46174618
isRecommended?: true;
4619+
insertText?: string;
46184620
}
46194621

46204622
export interface NewContentOptions {

src/services/completions.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ namespace ts.Completions {
167167
return undefined;
168168
}
169169
const { name, needsConvertPropertyAccess } = info;
170-
Debug.assert(!(needsConvertPropertyAccess && !propertyAccessToConvert));
171170
if (needsConvertPropertyAccess && !includeInsertTextCompletions) {
172171
return undefined;
173172
}
@@ -186,14 +185,24 @@ namespace ts.Completions {
186185
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
187186
sortText: "0",
188187
source: getSourceFromOrigin(origin),
189-
// TODO: GH#20619 Use configured quote style
190-
insertText: needsConvertPropertyAccess ? `["${name}"]` : undefined,
191-
replacementSpan: needsConvertPropertyAccess
192-
? createTextSpanFromBounds(findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!.getStart(sourceFile), propertyAccessToConvert.name.end)
193-
: undefined,
194-
hasAction: trueOrUndefined(needsConvertPropertyAccess || origin !== undefined),
188+
hasAction: trueOrUndefined(origin !== undefined),
195189
isRecommended: trueOrUndefined(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)),
190+
...getInsertTextAndReplacementSpan(),
196191
};
192+
193+
function getInsertTextAndReplacementSpan(): { insertText?: string, replacementSpan?: TextSpan } {
194+
if (kind === CompletionKind.Global) {
195+
if (typeChecker.isMemberSymbol(symbol)) {
196+
return { insertText: needsConvertPropertyAccess ? `this["${name}"]` : `this.${name}` };
197+
}
198+
}
199+
if (needsConvertPropertyAccess) {
200+
// TODO: GH#20619 Use configured quote style
201+
const replacementSpan = createTextSpanFromBounds(findChildOfKind(propertyAccessToConvert!, SyntaxKind.DotToken, sourceFile)!.getStart(sourceFile), propertyAccessToConvert!.name.end);
202+
return { insertText: `["${name}"]`, replacementSpan };
203+
}
204+
return {};
205+
}
197206
}
198207

199208

@@ -1097,6 +1106,15 @@ namespace ts.Completions {
10971106
const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
10981107

10991108
symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings);
1109+
1110+
// Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions`
1111+
if (options.includeInsertTextCompletions && scopeNode.kind !== SyntaxKind.SourceFile) {
1112+
const thisType = typeChecker.tryGetThisTypeAt(scopeNode);
1113+
if (thisType) {
1114+
symbols.push(...getPropertiesForCompletion(thisType, typeChecker, /*isForAccess*/ true));
1115+
}
1116+
}
1117+
11001118
if (options.includeExternalModuleExports) {
11011119
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", target);
11021120
}
@@ -2052,13 +2070,13 @@ namespace ts.Completions {
20522070
if (isIdentifierText(name, target)) return validIdentiferResult;
20532071
switch (kind) {
20542072
case CompletionKind.None:
2055-
case CompletionKind.Global:
20562073
case CompletionKind.MemberLike:
20572074
return undefined;
20582075
case CompletionKind.ObjectPropertyDeclaration:
20592076
// TODO: GH#18169
20602077
return { name: JSON.stringify(name), needsConvertPropertyAccess: false };
20612078
case CompletionKind.PropertyAccess:
2079+
case CompletionKind.Global:
20622080
// Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547
20632081
return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true };
20642082
case CompletionKind.String:

tests/cases/fourslash/completionListInScope.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//// interface localInterface {}
1414
//// export interface exportedInterface {}
1515
////
16-
//// module localModule {
16+
//// module localModule {
1717
//// export var x = 0;
1818
//// }
1919
//// export module exportedModule {
@@ -38,7 +38,7 @@
3838
//// interface localInterface2 {}
3939
//// export interface exportedInterface2 {}
4040
////
41-
//// module localModule2 {
41+
//// module localModule2 {
4242
//// export var x = 0;
4343
//// }
4444
//// export module exportedModule2 {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////class C {
4+
//// "foo bar": number;
5+
//// xyz() {
6+
//// /**/
7+
//// }
8+
////}
9+
////
10+
////function f(this: { x: number }) { /*f*/ }
11+
12+
goTo.marker("");
13+
14+
verify.completionListContains("xyz", "(method) C.xyz(): void", "", "method", undefined, undefined, {
15+
includeInsertTextCompletions: true,
16+
insertText: "this.xyz",
17+
});
18+
19+
verify.completionListContains("foo bar", '(property) C["foo bar"]: number', "", "property", undefined, undefined, {
20+
includeInsertTextCompletions: true,
21+
insertText: 'this["foo bar"]',
22+
});
23+
24+
goTo.marker("f");
25+
26+
verify.completionListContains("x", "(property) x: number", "", "property", undefined, undefined, {
27+
includeInsertTextCompletions: true,
28+
insertText: "this.x",
29+
});

tests/cases/fourslash/fourslash.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,13 @@ declare namespace FourSlashInterface {
151151
kind?: string | { kind?: string, kindModifiers?: string },
152152
spanIndex?: number,
153153
hasAction?: boolean,
154-
options?: { includeExternalModuleExports?: boolean, sourceDisplay?: string, isRecommended?: true },
154+
options?: {
155+
includeExternalModuleExports?: boolean,
156+
includeInsertTextCompletions?: boolean,
157+
sourceDisplay?: string,
158+
isRecommended?: true,
159+
insertText?: string,
160+
},
155161
): void;
156162
completionListItemsCountIsGreaterThan(count: number): void;
157163
completionListIsEmpty(): void;

0 commit comments

Comments
 (0)