Skip to content

Commit 8ed885d

Browse files
author
Andy
authored
Add completions from the 'this' type (#21231)
* Add completions from the 'this' type * Code review
1 parent 48ac301 commit 8ed885d

File tree

7 files changed

+93
-25
lines changed

7 files changed

+93
-25
lines changed

src/compiler/checker.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ namespace ts {
295295
getAccessibleSymbolChain,
296296
getTypePredicateOfSignature,
297297
resolveExternalModuleSymbol,
298+
tryGetThisTypeAt: node => {
299+
node = getParseTreeNode(node);
300+
return node && tryGetThisTypeAt(node);
301+
},
298302
};
299303

300304
const tupleTypes: GenericType[] = [];
@@ -13268,6 +13272,16 @@ namespace ts {
1326813272
if (needToCaptureLexicalThis) {
1326913273
captureLexicalThis(node, container);
1327013274
}
13275+
13276+
const type = tryGetThisTypeAt(node, container);
13277+
if (!type && noImplicitThis) {
13278+
// With noImplicitThis, functions may not reference 'this' if it has type 'any'
13279+
error(node, Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation);
13280+
}
13281+
return type || anyType;
13282+
}
13283+
13284+
function tryGetThisTypeAt(node: Node, container = getThisContainer(node, /*includeArrowFunctions*/ false)): Type | undefined {
1327113285
if (isFunctionLike(container) &&
1327213286
(!isInParameterInitializerBeforeContainingFunction(node) || getThisParameter(container))) {
1327313287
// Note: a parameter initializer should refer to class-this unless function-this is explicitly annotated.
@@ -13306,12 +13320,6 @@ namespace ts {
1330613320
return type;
1330713321
}
1330813322
}
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;
1331513323
}
1331613324

1331713325
function getTypeForThisExpressionFromJSDoc(node: Node) {

src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2921,6 +2921,8 @@ namespace ts {
29212921
/* @internal */ getAccessibleSymbolChain(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean): Symbol[] | undefined;
29222922
/* @internal */ getTypePredicateOfSignature(signature: Signature): TypePredicate;
29232923
/* @internal */ resolveExternalModuleSymbol(symbol: Symbol): Symbol;
2924+
/** @param node A location where we might consider accessing `this`. Not necessarily a ThisExpression. */
2925+
/* @internal */ tryGetThisTypeAt(node: Node): Type | undefined;
29242926
}
29252927

29262928
/* @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

+36-15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
namespace ts.Completions {
55
export type Log = (message: string) => void;
66

7-
interface SymbolOriginInfo {
7+
type SymbolOriginInfo = { type: "this-type" } | SymbolOriginInfoExport;
8+
interface SymbolOriginInfoExport {
9+
type: "export";
810
moduleSymbol: Symbol;
911
isDefaultExport: boolean;
1012
}
@@ -170,11 +172,21 @@ namespace ts.Completions {
170172
return undefined;
171173
}
172174
const { name, needsConvertPropertyAccess } = info;
173-
Debug.assert(!(needsConvertPropertyAccess && !propertyAccessToConvert));
174175
if (needsConvertPropertyAccess && !includeInsertTextCompletions) {
175176
return undefined;
176177
}
177178

179+
let insertText: string | undefined;
180+
let replacementSpan: TextSpan | undefined;
181+
if (kind === CompletionKind.Global && origin && origin.type === "this-type") {
182+
insertText = needsConvertPropertyAccess ? `this["${name}"]` : `this.${name}`;
183+
}
184+
else if (needsConvertPropertyAccess) {
185+
// TODO: GH#20619 Use configured quote style
186+
insertText = `["${name}"]`;
187+
replacementSpan = createTextSpanFromBounds(findChildOfKind(propertyAccessToConvert!, SyntaxKind.DotToken, sourceFile)!.getStart(sourceFile), propertyAccessToConvert!.name.end);
188+
}
189+
178190
// TODO(drosen): Right now we just permit *all* semantic meanings when calling
179191
// 'getSymbolKind' which is permissible given that it is backwards compatible; but
180192
// really we should consider passing the meaning for the node so that we don't report
@@ -189,13 +201,10 @@ namespace ts.Completions {
189201
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
190202
sortText: "0",
191203
source: getSourceFromOrigin(origin),
192-
// TODO: GH#20619 Use configured quote style
193-
insertText: needsConvertPropertyAccess ? `["${name}"]` : undefined,
194-
replacementSpan: needsConvertPropertyAccess
195-
? createTextSpanFromBounds(findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!.getStart(sourceFile), propertyAccessToConvert.name.end)
196-
: undefined,
197-
hasAction: trueOrUndefined(needsConvertPropertyAccess || origin !== undefined),
204+
hasAction: trueOrUndefined(!!origin && origin.type === "export"),
198205
isRecommended: trueOrUndefined(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)),
206+
insertText,
207+
replacementSpan,
199208
};
200209
}
201210

@@ -210,7 +219,7 @@ namespace ts.Completions {
210219
}
211220

212221
function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined {
213-
return origin && stripQuotes(origin.moduleSymbol.name);
222+
return origin && origin.type === "export" ? stripQuotes(origin.moduleSymbol.name) : undefined;
214223
}
215224

216225
function getCompletionEntriesFromSymbols(
@@ -504,7 +513,7 @@ namespace ts.Completions {
504513
}
505514

506515
function getSymbolName(symbol: Symbol, origin: SymbolOriginInfo | undefined, target: ScriptTarget): string {
507-
return origin && origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default
516+
return origin && origin.type === "export" && origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default
508517
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
509518
? firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined)
510519
|| codefix.moduleSymbolToValidIdentifier(origin.moduleSymbol, target)
@@ -590,13 +599,13 @@ namespace ts.Completions {
590599
allSourceFiles: ReadonlyArray<SourceFile>,
591600
): CodeActionsAndSourceDisplay {
592601
const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)];
593-
return symbolOriginInfo
602+
return symbolOriginInfo && symbolOriginInfo.type === "export"
594603
? getCodeActionsAndSourceDisplayForImport(symbolOriginInfo, symbol, program, checker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, allSourceFiles)
595604
: { codeActions: undefined, sourceDisplay: undefined };
596605
}
597606

598607
function getCodeActionsAndSourceDisplayForImport(
599-
symbolOriginInfo: SymbolOriginInfo,
608+
symbolOriginInfo: SymbolOriginInfoExport,
600609
symbol: Symbol,
601610
program: Program,
602611
checker: TypeChecker,
@@ -1117,6 +1126,18 @@ namespace ts.Completions {
11171126
const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
11181127

11191128
symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings);
1129+
1130+
// Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions`
1131+
if (options.includeInsertTextCompletions && scopeNode.kind !== SyntaxKind.SourceFile) {
1132+
const thisType = typeChecker.tryGetThisTypeAt(scopeNode);
1133+
if (thisType) {
1134+
for (const symbol of getPropertiesForCompletion(thisType, typeChecker, /*isForAccess*/ true)) {
1135+
symbolToOriginInfoMap[getSymbolId(symbol)] = { type: "this-type" };
1136+
symbols.push(symbol);
1137+
}
1138+
}
1139+
}
1140+
11201141
if (options.includeExternalModuleExports) {
11211142
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", target);
11221143
}
@@ -1230,10 +1251,10 @@ namespace ts.Completions {
12301251
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
12311252
}
12321253

1233-
const origin: SymbolOriginInfo = { moduleSymbol, isDefaultExport };
1254+
const origin: SymbolOriginInfo = { type: "export", moduleSymbol, isDefaultExport };
12341255
if (stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
12351256
symbols.push(symbol);
1236-
symbolToOriginInfoMap[getSymbolId(symbol)] = { moduleSymbol, isDefaultExport };
1257+
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;
12371258
}
12381259
}
12391260
});
@@ -2072,13 +2093,13 @@ namespace ts.Completions {
20722093
if (isIdentifierText(name, target)) return validIdentiferResult;
20732094
switch (kind) {
20742095
case CompletionKind.None:
2075-
case CompletionKind.Global:
20762096
case CompletionKind.MemberLike:
20772097
return undefined;
20782098
case CompletionKind.ObjectPropertyDeclaration:
20792099
// TODO: GH#18169
20802100
return { name: JSON.stringify(name), needsConvertPropertyAccess: false };
20812101
case CompletionKind.PropertyAccess:
2102+
case CompletionKind.Global:
20822103
// Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547
20832104
return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true };
20842105
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)