Skip to content

Commit 312976f

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

File tree

7 files changed

+124
-45
lines changed

7 files changed

+124
-45
lines changed

src/compiler/checker.ts

Lines changed: 54 additions & 33 deletions
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, /*isForTest*/ true);
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[] = [];
@@ -13221,13 +13230,24 @@ namespace ts {
1322113230
}
1322213231

1322313232
function checkThisExpression(node: Node): Type {
13233+
const type = tryGetThisTypeAt(node, /*isForTest*/ false);
13234+
if (type) return type;
13235+
13236+
if (noImplicitThis) {
13237+
// With noImplicitThis, functions may not reference 'this' if it has type 'any'
13238+
error(node, Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation);
13239+
}
13240+
return anyType;
13241+
}
13242+
13243+
function tryGetThisTypeAt(node: Node, isForTest: boolean): Type | undefined {
1322413244
// Stop at the first arrow function so that we can
1322513245
// tell whether 'this' needs to be captured.
1322613246
let container = getThisContainer(node, /* includeArrowFunctions */ true);
1322713247
let needToCaptureLexicalThis = false;
1322813248

1322913249
if (container.kind === SyntaxKind.Constructor) {
13230-
checkThisBeforeSuper(node, container, Diagnostics.super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class);
13250+
if (!isForTest) checkThisBeforeSuper(node, container, Diagnostics.super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class);
1323113251
}
1323213252

1323313253
// Now skip arrow functions to get the "real" owner of 'this'.
@@ -13238,36 +13258,14 @@ namespace ts {
1323813258
needToCaptureLexicalThis = (languageVersion < ScriptTarget.ES2015);
1323913259
}
1324013260

13241-
switch (container.kind) {
13242-
case SyntaxKind.ModuleDeclaration:
13243-
error(node, Diagnostics.this_cannot_be_referenced_in_a_module_or_namespace_body);
13244-
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13245-
break;
13246-
case SyntaxKind.EnumDeclaration:
13247-
error(node, Diagnostics.this_cannot_be_referenced_in_current_location);
13248-
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13249-
break;
13250-
case SyntaxKind.Constructor:
13251-
if (isInConstructorArgumentInitializer(node, container)) {
13252-
error(node, Diagnostics.this_cannot_be_referenced_in_constructor_arguments);
13253-
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13254-
}
13255-
break;
13256-
case SyntaxKind.PropertyDeclaration:
13257-
case SyntaxKind.PropertySignature:
13258-
if (hasModifier(container, ModifierFlags.Static)) {
13259-
error(node, Diagnostics.this_cannot_be_referenced_in_a_static_property_initializer);
13260-
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13261-
}
13262-
break;
13263-
case SyntaxKind.ComputedPropertyName:
13264-
error(node, Diagnostics.this_cannot_be_referenced_in_a_computed_property_name);
13265-
break;
13266-
}
13261+
if (!isForTest) {
13262+
reportErrorsForThisExpression(node, container);
1326713263

13268-
if (needToCaptureLexicalThis) {
13269-
captureLexicalThis(node, container);
13264+
if (needToCaptureLexicalThis) {
13265+
captureLexicalThis(node, container);
13266+
}
1327013267
}
13268+
1327113269
if (isFunctionLike(container) &&
1327213270
(!isInParameterInitializerBeforeContainingFunction(node) || getThisParameter(container))) {
1327313271
// Note: a parameter initializer should refer to class-this unless function-this is explicitly annotated.
@@ -13306,12 +13304,35 @@ namespace ts {
1330613304
return type;
1330713305
}
1330813306
}
13307+
}
1330913308

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);
13309+
function reportErrorsForThisExpression(node: Node, container: Node): void {
13310+
switch (container.kind) {
13311+
case SyntaxKind.ModuleDeclaration:
13312+
error(node, Diagnostics.this_cannot_be_referenced_in_a_module_or_namespace_body);
13313+
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13314+
break;
13315+
case SyntaxKind.EnumDeclaration:
13316+
error(node, Diagnostics.this_cannot_be_referenced_in_current_location);
13317+
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13318+
break;
13319+
case SyntaxKind.Constructor:
13320+
if (isInConstructorArgumentInitializer(node, container)) {
13321+
error(node, Diagnostics.this_cannot_be_referenced_in_constructor_arguments);
13322+
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13323+
}
13324+
break;
13325+
case SyntaxKind.PropertyDeclaration:
13326+
case SyntaxKind.PropertySignature:
13327+
if (hasModifier(container, ModifierFlags.Static)) {
13328+
error(node, Diagnostics.this_cannot_be_referenced_in_a_static_property_initializer);
13329+
// do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks
13330+
}
13331+
break;
13332+
case SyntaxKind.ComputedPropertyName:
13333+
error(node, Diagnostics.this_cannot_be_referenced_in_a_computed_property_name);
13334+
break;
1331313335
}
13314-
return anyType;
1331513336
}
1331613337

1331713338
function getTypeForThisExpressionFromJSDoc(node: Node) {

src/compiler/types.ts

Lines changed: 3 additions & 0 deletions
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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 26 additions & 8 deletions
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

Lines changed: 2 additions & 2 deletions
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 {
Lines changed: 29 additions & 0 deletions
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

Lines changed: 7 additions & 1 deletion
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)