Skip to content

Commit 23b500c

Browse files
authored
Don’t offer this. completions on self, window, global, globalThis. Disambiguate this. completions from others in details requests. (microsoft#37652)
* Special-case window, self, global, globalThis methods * Disambiguate global and this property completions in details requests * Hide the Map<boolean> implementation * Update old tests * Replace SymbolOriginKind stringification with dedicated enum
1 parent 065a996 commit 23b500c

10 files changed

+193
-34
lines changed

src/compiler/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3608,7 +3608,7 @@ namespace ts {
36083608
* Does not include properties of primitive types.
36093609
*/
36103610
/* @internal */ getAllPossiblePropertiesOfTypes(type: readonly Type[]): Symbol[];
3611-
/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags, excludeGlobals: boolean): Symbol | undefined;
3611+
/* @internal */ resolveName(name: string, location: Node | undefined, meaning: SymbolFlags, excludeGlobals: boolean): Symbol | undefined;
36123612
/* @internal */ getJsxNamespace(location?: Node): string;
36133613

36143614
/**

src/harness/fourslashImpl.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,14 @@ namespace FourSlash {
322322
if (!resolvedResult.isLibFile) {
323323
this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName,
324324
Harness.Compiler.getDefaultLibrarySourceFile()!.text, /*isRootFile*/ false);
325+
326+
compilationOptions.lib?.forEach(fileName => {
327+
const libFile = Harness.Compiler.getDefaultLibrarySourceFile(fileName);
328+
ts.Debug.assertIsDefined(libFile, `Could not find lib file '${fileName}'`);
329+
if (libFile) {
330+
this.languageServiceAdapterHost.addScript(fileName, libFile.text, /*isRootFile*/ false);
331+
}
332+
});
325333
}
326334
}
327335
else {
@@ -334,6 +342,14 @@ namespace FourSlash {
334342
if (!compilationOptions.noLib) {
335343
this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName,
336344
Harness.Compiler.getDefaultLibrarySourceFile()!.text, /*isRootFile*/ false);
345+
346+
compilationOptions.lib?.forEach(fileName => {
347+
const libFile = Harness.Compiler.getDefaultLibrarySourceFile(fileName);
348+
ts.Debug.assertIsDefined(libFile, `Could not find lib file '${fileName}'`);
349+
if (libFile) {
350+
this.languageServiceAdapterHost.addScript(fileName, libFile.text, /*isRootFile*/ false);
351+
}
352+
});
337353
}
338354
}
339355

@@ -879,19 +895,19 @@ namespace FourSlash {
879895
}
880896
}
881897

882-
assert.equal(actual.hasAction, hasAction, `Expected 'hasAction' value '${actual.hasAction}' to equal '${hasAction}'`);
883-
assert.equal(actual.isRecommended, isRecommended, `Expected 'isRecommended' value '${actual.isRecommended}' to equal '${isRecommended}'`);
884-
assert.equal(actual.source, source, `Expected 'source' value '${actual.source}' to equal '${source}'`);
898+
assert.equal(actual.hasAction, hasAction, `Expected 'hasAction' properties to match`);
899+
assert.equal(actual.isRecommended, isRecommended, `Expected 'isRecommended' properties to match'`);
900+
assert.equal(actual.source, source, `Expected 'source' values to match`);
885901
assert.equal(actual.sortText, sortText || ts.Completions.SortText.LocationPriority, this.messageAtLastKnownMarker(`Actual entry: ${JSON.stringify(actual)}`));
886902

887903
if (text !== undefined) {
888904
const actualDetails = this.getCompletionEntryDetails(actual.name, actual.source)!;
889-
assert.equal(ts.displayPartsToString(actualDetails.displayParts), text);
890-
assert.equal(ts.displayPartsToString(actualDetails.documentation), documentation || "");
905+
assert.equal(ts.displayPartsToString(actualDetails.displayParts), text, "Expected 'text' property to match 'displayParts' string");
906+
assert.equal(ts.displayPartsToString(actualDetails.documentation), documentation || "", "Expected 'documentation' property to match 'documentation' display parts string");
891907
// TODO: GH#23587
892908
// assert.equal(actualDetails.kind, actual.kind);
893-
assert.equal(actualDetails.kindModifiers, actual.kindModifiers);
894-
assert.equal(actualDetails.source && ts.displayPartsToString(actualDetails.source), sourceDisplay);
909+
assert.equal(actualDetails.kindModifiers, actual.kindModifiers, "Expected 'kindModifiers' properties to match");
910+
assert.equal(actualDetails.source && ts.displayPartsToString(actualDetails.source), sourceDisplay, "Expected 'sourceDisplay' property to match 'source' display parts string");
895911
assert.deepEqual(actualDetails.tags, tags);
896912
}
897913
else {

src/harness/fourslashInterfaceImpl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ namespace FourSlashInterface {
845845
}
846846
export namespace Completion {
847847
export import SortText = ts.Completions.SortText;
848+
export import CompletionSource = ts.Completions.CompletionSource;
848849

849850
const functionEntry = (name: string): ExpectedCompletionEntryObject => ({
850851
name,

src/services/completions.ts

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ namespace ts.Completions {
1111
}
1212
export type Log = (message: string) => void;
1313

14+
/**
15+
* Special values for `CompletionInfo['source']` used to disambiguate
16+
* completion items with the same `name`. (Each completion item must
17+
* have a unique name/source combination, because those two fields
18+
* comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`.
19+
*
20+
* When the completion item is an auto-import suggestion, the source
21+
* is the module specifier of the suggestion. To avoid collisions,
22+
* the values here should not be a module specifier we would ever
23+
* generate for an auto-import.
24+
*/
25+
export enum CompletionSource {
26+
/** Completions that require `this.` insertion text */
27+
ThisProperty = "ThisProperty/"
28+
}
29+
1430
const enum SymbolOriginInfoKind {
1531
ThisType = 1 << 0,
1632
SymbolMember = 1 << 1,
@@ -52,6 +68,11 @@ namespace ts.Completions {
5268
return !!(origin.kind & SymbolOriginInfoKind.Nullable);
5369
}
5470

71+
interface UniqueNameSet {
72+
add(name: string): void;
73+
has(name: string): boolean;
74+
}
75+
5576
/**
5677
* Map from symbol id -> SymbolOriginInfo.
5778
* Only populated for symbols that come from other modules.
@@ -298,7 +319,7 @@ namespace ts.Completions {
298319
function getJSCompletionEntries(
299320
sourceFile: SourceFile,
300321
position: number,
301-
uniqueNames: Map<true>,
322+
uniqueNames: UniqueNameSet,
302323
target: ScriptTarget,
303324
entries: Push<CompletionEntry>): void {
304325
getNameTable(sourceFile).forEach((pos, name) => {
@@ -307,7 +328,8 @@ namespace ts.Completions {
307328
return;
308329
}
309330
const realName = unescapeLeadingUnderscores(name);
310-
if (addToSeen(uniqueNames, realName) && isIdentifierText(realName, target)) {
331+
if (!uniqueNames.has(realName) && isIdentifierText(realName, target)) {
332+
uniqueNames.add(realName);
311333
entries.push({
312334
name: realName,
313335
kind: ScriptElementKind.warning,
@@ -429,7 +451,12 @@ namespace ts.Completions {
429451
}
430452

431453
function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined {
432-
return origin && originIsExport(origin) ? stripQuotes(origin.moduleSymbol.name) : undefined;
454+
if (originIsExport(origin)) {
455+
return stripQuotes(origin.moduleSymbol.name);
456+
}
457+
if (origin?.kind === SymbolOriginInfoKind.ThisType) {
458+
return CompletionSource.ThisProperty;
459+
}
433460
}
434461

435462
export function getCompletionEntriesFromSymbols(
@@ -448,21 +475,21 @@ namespace ts.Completions {
448475
recommendedCompletion?: Symbol,
449476
symbolToOriginInfoMap?: SymbolOriginInfoMap,
450477
symbolToSortTextMap?: SymbolSortTextMap,
451-
): Map<true> {
478+
): UniqueNameSet {
452479
const start = timestamp();
453480
// Tracks unique names.
454-
// We don't set this for global variables or completions from external module exports, because we can have multiple of those.
455-
// Based on the order we add things we will always see locals first, then globals, then module exports.
481+
// Value is set to false for global variables or completions from external module exports, because we can have multiple of those;
482+
// true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports.
456483
// So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name.
457-
const uniques = createMap<true>();
484+
const uniques = createMap<boolean>();
458485
for (const symbol of symbols) {
459486
const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined;
460487
const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected);
461488
if (!info) {
462489
continue;
463490
}
464491
const { name, needsConvertPropertyAccess } = info;
465-
if (uniques.has(name)) {
492+
if (uniques.get(name)) {
466493
continue;
467494
}
468495

@@ -484,16 +511,22 @@ namespace ts.Completions {
484511
continue;
485512
}
486513

487-
// Latter case tests whether this is a global variable.
488-
if (!origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile()))) { // TODO: GH#18217
489-
uniques.set(name, true);
490-
}
514+
/** True for locals; false for globals, module exports from other files, `this.` completions. */
515+
const shouldShadowLaterSymbols = !origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile()));
516+
uniques.set(name, shouldShadowLaterSymbols);
491517

492518
entries.push(entry);
493519
}
494520

495521
log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start));
496-
return uniques;
522+
523+
// Prevent consumers of this map from having to worry about
524+
// the boolean value. Externally, it should be seen as the
525+
// set of all names.
526+
return {
527+
has: name => uniques.has(name),
528+
add: name => uniques.set(name, true),
529+
};
497530
}
498531

499532
function getLabelCompletionAtPosition(node: BreakOrContinueStatement): CompletionInfo | undefined {
@@ -1359,7 +1392,7 @@ namespace ts.Completions {
13591392
// Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions`
13601393
if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== SyntaxKind.SourceFile) {
13611394
const thisType = typeChecker.tryGetThisTypeAt(scopeNode, /*includeGlobalThis*/ false);
1362-
if (thisType) {
1395+
if (thisType && !isProbablyGlobalType(thisType, sourceFile, typeChecker)) {
13631396
for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) {
13641397
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType };
13651398
symbols.push(symbol);
@@ -2723,13 +2756,22 @@ namespace ts.Completions {
27232756
}
27242757
}
27252758

2726-
function isNonGlobalDeclaration(declaration: Declaration) {
2727-
const sourceFile = declaration.getSourceFile();
2728-
// If the file is not a module, the declaration is global
2729-
if (!sourceFile.externalModuleIndicator && !sourceFile.commonJsModuleIndicator) {
2730-
return false;
2759+
/** Determines if a type is exactly the same type resolved by the global 'self', 'global', or 'globalThis'. */
2760+
function isProbablyGlobalType(type: Type, sourceFile: SourceFile, checker: TypeChecker) {
2761+
// The type of `self` and `window` is the same in lib.dom.d.ts, but `window` does not exist in
2762+
// lib.webworker.d.ts, so checking against `self` is also a check against `window` when it exists.
2763+
const selfSymbol = checker.resolveName("self", /*location*/ undefined, SymbolFlags.Value, /*excludeGlobals*/ false);
2764+
if (selfSymbol && checker.getTypeOfSymbolAtLocation(selfSymbol, sourceFile) === type) {
2765+
return true;
2766+
}
2767+
const globalSymbol = checker.resolveName("global", /*location*/ undefined, SymbolFlags.Value, /*excludeGlobals*/ false);
2768+
if (globalSymbol && checker.getTypeOfSymbolAtLocation(globalSymbol, sourceFile) === type) {
2769+
return true;
2770+
}
2771+
const globalThisSymbol = checker.resolveName("globalThis", /*location*/ undefined, SymbolFlags.Value, /*excludeGlobals*/ false);
2772+
if (globalThisSymbol && checker.getTypeOfSymbolAtLocation(globalThisSymbol, sourceFile) === type) {
2773+
return true;
27312774
}
2732-
// If the file is a module written in TypeScript, it still might be in a `declare global` augmentation
2733-
return isInJSFile(declaration) || !findAncestor(declaration, isGlobalScopeAugmentation);
2775+
return false;
27342776
}
27352777
}

src/services/utilities.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2775,5 +2775,19 @@ namespace ts {
27752775
return name.charCodeAt(0) === CharacterCodes._;
27762776
}
27772777

2778+
export function isGlobalDeclaration(declaration: Declaration) {
2779+
return !isNonGlobalDeclaration(declaration);
2780+
}
2781+
2782+
export function isNonGlobalDeclaration(declaration: Declaration) {
2783+
const sourceFile = declaration.getSourceFile();
2784+
// If the file is not a module, the declaration is global
2785+
if (!sourceFile.externalModuleIndicator && !sourceFile.commonJsModuleIndicator) {
2786+
return false;
2787+
}
2788+
// If the file is a module written in TypeScript, it still might be in a `declare global` augmentation
2789+
return isInJSFile(declaration) || !findAncestor(declaration, isGlobalScopeAugmentation);
2790+
}
2791+
27782792
// #endregion
27792793
}

tests/cases/fourslash/completionsJsxAttributeInitializer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ verify.completions({
99
marker: "",
1010
includes: [
1111
{ name: "x", text: "(parameter) x: number", kind: "parameter", insertText: "{x}" },
12-
{ name: "p", text: "(JSX attribute) p: number", kind: "JSX attribute", insertText: "{this.p}", sortText: completion.SortText.SuggestedClassMembers },
13-
{ name: "a b", text: '(JSX attribute) "a b": number', kind: "JSX attribute", insertText: '{this["a b"]}', sortText: completion.SortText.SuggestedClassMembers },
12+
{ name: "p", text: "(JSX attribute) p: number", kind: "JSX attribute", insertText: "{this.p}", sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty },
13+
{ name: "a b", text: '(JSX attribute) "a b": number', kind: "JSX attribute", insertText: '{this["a b"]}', sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty },
1414
],
1515
preferences: {
1616
includeInsertTextCompletions: true,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: globals.d.ts
4+
//// declare var foot: string;
5+
6+
// @Filename: index.ts
7+
//// class Service {
8+
//// foot!: number;
9+
//// serve() {
10+
//// foot/**/
11+
//// }
12+
//// }
13+
14+
15+
verify.completions({
16+
marker: "",
17+
exact: [
18+
"arguments",
19+
completion.globalThisEntry,
20+
...completion.globalsVars,
21+
{
22+
name: "foot",
23+
insertText: undefined,
24+
kind: "var",
25+
kindModifiers: "declare",
26+
sortText: completion.SortText.GlobalsOrKeywords,
27+
text: "var foot: string"
28+
},
29+
"Service",
30+
completion.undefinedVarEntry,
31+
{
32+
name: "foot",
33+
insertText: "this.foot",
34+
kind: "property",
35+
sortText: completion.SortText.SuggestedClassMembers,
36+
source: completion.CompletionSource.ThisProperty,
37+
text: "(property) Service.foot: number"
38+
},
39+
{
40+
name: "serve",
41+
insertText: "this.serve",
42+
kind: "method",
43+
sortText: completion.SortText.SuggestedClassMembers,
44+
source: completion.CompletionSource.ThisProperty
45+
},
46+
...completion.insideMethodKeywords
47+
],
48+
preferences: {
49+
includeInsertTextCompletions: true
50+
}
51+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// #37091
4+
5+
// @lib: dom
6+
// @allowJs: true
7+
8+
// @Filename: globals.d.ts
9+
//// declare var global: { console: Console };
10+
11+
// @Filename: index.js
12+
//// window.f = function() { console/*1*/ }
13+
//// self.g = function() { console/*2*/ }
14+
//// global.h = function() { console/*3*/ }
15+
//// globalThis.i = function() { console/*4*/ }
16+
17+
test.markerNames().forEach(marker => {
18+
verify.completions({
19+
marker,
20+
includes: [{
21+
name: "console",
22+
insertText: undefined,
23+
kind: "var",
24+
kindModifiers: "declare",
25+
sortText: completion.SortText.GlobalsOrKeywords
26+
}]
27+
}, {
28+
preferences: {
29+
includeInsertTextCompletions: true
30+
}
31+
});
32+
});

tests/cases/fourslash/completionsThisType.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ verify.completions(
1414
{
1515
marker: "",
1616
includes: [
17-
{ name: "xyz", text: "(method) C.xyz(): any", kind: "method", insertText: "this.xyz", sortText: completion.SortText.SuggestedClassMembers },
18-
{ name: "foo bar", text: '(property) C["foo bar"]: number', kind: "property", insertText: 'this["foo bar"]', sortText: completion.SortText.SuggestedClassMembers },
17+
{ name: "xyz", text: "(method) C.xyz(): any", kind: "method", insertText: "this.xyz", sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty },
18+
{ name: "foo bar", text: '(property) C["foo bar"]: number', kind: "property", insertText: 'this["foo bar"]', sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty },
1919
],
2020
isNewIdentifierLocation: true,
2121
preferences,
2222
},
2323
{
2424
marker: "f",
25-
includes: { name: "x", text: "(property) x: number", kind: "property", insertText: "this.x", sortText: completion.SortText.SuggestedClassMembers },
25+
includes: { name: "x", text: "(property) x: number", kind: "property", insertText: "this.x", sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty },
2626
preferences,
2727
},
2828
);

tests/cases/fourslash/fourslash.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,9 @@ declare namespace completion {
753753
AutoImportSuggestions = "5",
754754
JavascriptIdentifiers = "6"
755755
}
756+
export const enum CompletionSource {
757+
ThisProperty = "ThisProperty/"
758+
}
756759
export const globalThisEntry: Entry;
757760
export const undefinedVarEntry: Entry;
758761
export const globals: ReadonlyArray<Entry>;

0 commit comments

Comments
 (0)