Skip to content

Commit 6ad25af

Browse files
committed
Rename things and make comments better
1 parent 306a1c2 commit 6ad25af

File tree

1 file changed

+80
-38
lines changed

1 file changed

+80
-38
lines changed

src/services/completions.ts

Lines changed: 80 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,23 +1255,60 @@ namespace ts.Completions {
12551255
typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules));
12561256
}
12571257

1258-
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget, host: LanguageServiceHost): void {
1258+
/**
1259+
* Gathers symbols that can be imported from other files, deduplicating along the way. Symbols can be “duplicates”
1260+
* if re-exported from another module, e.g. `export { foo } from "./a"`. That syntax creates a fresh symbol, but
1261+
* it’s just an alias to the first, and both have the same name, so we generally want to filter those aliases out,
1262+
* if and only if the the first can be imported (it may be excluded due to package.json filtering in
1263+
* `codefix.forEachExternalModuleToImportFrom`).
1264+
*
1265+
* Example. Imagine a chain of node_modules re-exporting one original symbol:
1266+
*
1267+
* ```js
1268+
* node_modules/x/index.js node_modules/y/index.js node_modules/z/index.js
1269+
* +-----------------------+ +--------------------------+ +--------------------------+
1270+
* | | | | | |
1271+
* | export const foo = 0; | <--- | export { foo } from 'x'; | <--- | export { foo } from 'y'; |
1272+
* | | | | | |
1273+
* +-----------------------+ +--------------------------+ +--------------------------+
1274+
* ```
1275+
*
1276+
* Also imagine three buckets, which we’ll reference soon:
1277+
*
1278+
* ```md
1279+
* | | | | | |
1280+
* | **Bucket A** | | **Bucket B** | | **Bucket C** |
1281+
* | Symbols to | | Aliases to symbols | | Symbols to return |
1282+
* | definitely | | in Buckets A or C | | if nothing better |
1283+
* | return | | (don’t return these) | | comes along |
1284+
* |__________________| |______________________| |___________________|
1285+
* ```
1286+
*
1287+
* We _probably_ want to show `foo` from 'x', but not from 'y' or 'z'. However, if 'x' is not in a package.json, it
1288+
* will not appear in a `forEachExternalModuleToImportFrom` iteration. Furthermore, the order of iterations is not
1289+
* guaranteed, as it is host-dependent. Therefore, when presented with the symbol `foo` from module 'y' alone, we
1290+
* may not be sure whether or not it should go in the list. So, we’ll take the following steps:
1291+
*
1292+
* 1. Resolve alias `foo` from 'y' to the export declaration in 'x', get the symbol there, and see if that symbol is
1293+
* already in Bucket A (symbols we already know will be returned). If it is, put `foo` from 'y' in Bucket B
1294+
* (symbols that are aliases to symbols in Bucket A). If it’s not, put it in Bucket C.
1295+
* 2. Next, imagine we see `foo` from module 'z'. Again, we resolve the alias to the nearest export, which is in 'y'.
1296+
* At this point, if that nearest export from 'y' is in _any_ of the three buckets, we know the symbol in 'z'
1297+
* should never be returned in the final list, so put it in Bucket B.
1298+
* 3. Next, imagine we see `foo` from module 'x', the original. Syntactically, it doesn’t look like a re-export, so
1299+
* we can just check Bucket C to see if we put any aliases to the original in there. If they exist, throw them out.
1300+
* Put this symbol in Bucket A.
1301+
* 4. After we’ve iterated through every symbol of every module, any symbol left in Bucket C means that step 3 didn’t
1302+
* occur for that symbol---that is, the original symbol is not in Bucket A, so we should include the alias. Move
1303+
* everything from Bucket C to Bucket A.
1304+
*/
1305+
function getSymbolsFromOtherSourceFileExports(/** Bucket A */ symbols: Symbol[], tokenText: string, target: ScriptTarget, host: LanguageServiceHost): void {
12591306
const tokenTextLowerCase = tokenText.toLowerCase();
12601307
const seenResolvedModules = createMap<true>();
1261-
/**
1262-
* Contains re-exported symbols that should only be pushed onto `symbols` if the symbol they’re aliases for
1263-
* don’t appear in `symbols`, which can happen if a re-exporting module is found in the project package.json
1264-
* but the original exporting module is not. These symbols are corralled here by the symbol id of the exported
1265-
* symbol that they’re re-exporting, and deleted from the map if that original exported symbol comes through.
1266-
* Any remaining in the map after all symbols have been seen can be assumed to be not duplicates.
1267-
*/
1268-
const potentialDuplicateSymbols = createMap<{ alias: Symbol, moduleSymbol: Symbol, insertAt: number }>();
1269-
/**
1270-
* Contains re-exported symbols that we’ve already skipped due to knowing their aliased symbol is already covered.
1271-
* By keeping track of this, we can short circuit when tracing re-exported re-exports instead of having to go all
1272-
* the way back to the source to find out if a symbol is a duplicate.
1273-
*/
1274-
const coveredReExportedSymbols = createMap<true>();
1308+
/** Bucket B */
1309+
const aliasesToAlreadyIncludedSymbols = createMap<true>();
1310+
/** Bucket C */
1311+
const aliasesToReturnIfOriginalsAreMissing = createMap<{ alias: Symbol, moduleSymbol: Symbol, insertAt: number }>();
12751312

12761313
codefix.forEachExternalModuleToImportFrom(typeChecker, host, preferences, program.redirectTargetsMap, sourceFile, program.getSourceFiles(), moduleSymbol => {
12771314
// Perf -- ignore other modules if this is a request for details
@@ -1295,10 +1332,6 @@ namespace ts.Completions {
12951332
}
12961333

12971334
for (const symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
1298-
// Don't add a completion for a re-export, only for the original.
1299-
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
1300-
// This is just to avoid adding duplicate completion entries.
1301-
//
13021335
// If `symbol.parent !== ...`, this is an `export * from "foo"` re-export. Those don't create new symbols.
13031336
if (typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol) {
13041337
continue;
@@ -1309,32 +1342,29 @@ namespace ts.Completions {
13091342
}
13101343
// If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
13111344
if (some(symbol.declarations, d => isExportSpecifier(d) && !d.propertyName && !!d.parent.parent.moduleSpecifier)) {
1312-
// If we haven’t yet seen the original symbol, note the alias so we can add it at the end if the original doesn’t show up eventually
1313-
const nearestExportSymbol = forEachAlias(typeChecker, symbol, alias => some(alias.declarations, d => isExportSpecifier(d) || !!d.localSymbol) && alias);
1314-
if (nearestExportSymbol) {
1315-
if (!symbolToOriginInfoMap[getSymbolId(nearestExportSymbol)] && !coveredReExportedSymbols.has(getSymbolId(nearestExportSymbol).toString())) {
1316-
potentialDuplicateSymbols.set(getSymbolId(nearestExportSymbol).toString(), { alias: symbol, moduleSymbol, insertAt: symbols.length });
1317-
}
1318-
else {
1319-
// Perf - we know this symbol is an alias to one that’s already covered in `symbols`, so store it here
1320-
// in case another symbol re-exports this one; that way we can short-circuit as soon as we see this symbol id.
1321-
addToSeen(coveredReExportedSymbols, getSymbolId(symbol));
1322-
}
1345+
// Walk the export chain back one module (step 1 or 2 in diagrammed example)
1346+
const nearestExportSymbol = Debug.assertDefined(getNearestExportSymbol(symbol));
1347+
if (!symbolHasBeenSeen(nearestExportSymbol)) {
1348+
aliasesToReturnIfOriginalsAreMissing.set(getSymbolId(nearestExportSymbol).toString(), { alias: symbol, moduleSymbol, insertAt: symbols.length });
1349+
aliasesToAlreadyIncludedSymbols.set(getSymbolId(symbol).toString(), true);
1350+
}
1351+
else {
1352+
// Perf - we know this symbol is an alias to one that’s already covered in `symbols`, so store it here
1353+
// in case another symbol re-exports this one; that way we can short-circuit as soon as we see this symbol id.
1354+
addToSeen(aliasesToAlreadyIncludedSymbols, getSymbolId(symbol));
13231355
}
1324-
continue;
13251356
}
13261357
else {
1327-
// This is not a re-export, so see if we have any aliases pending and remove them
1328-
potentialDuplicateSymbols.delete(getSymbolId(symbol).toString());
1358+
// This is not a re-export, so see if we have any aliases pending and remove them (step 3 in diagrammed example)
1359+
aliasesToReturnIfOriginalsAreMissing.delete(getSymbolId(symbol).toString());
1360+
pushSymbol(symbol, moduleSymbol);
13291361
}
1330-
1331-
pushSymbol(symbol, moduleSymbol);
13321362
}
13331363
});
13341364

13351365
// By this point, any potential duplicates that were actually duplicates have been
1336-
// removed, so the rest need to be added.
1337-
potentialDuplicateSymbols.forEach(({ alias, moduleSymbol, insertAt }) => pushSymbol(alias, moduleSymbol, insertAt));
1366+
// removed, so the rest need to be added. (Step 4 in diagrammed example)
1367+
aliasesToReturnIfOriginalsAreMissing.forEach(({ alias, moduleSymbol, insertAt }) => pushSymbol(alias, moduleSymbol, insertAt));
13381368

13391369
function pushSymbol(symbol: Symbol, moduleSymbol: Symbol, insertAt = symbols.length) {
13401370
const isDefaultExport = symbol.escapedName === InternalSymbolName.Default;
@@ -1347,8 +1377,20 @@ namespace ts.Completions {
13471377
symbolToSortTextMap[getSymbolId(symbol)] = SortText.AutoImportSuggestions;
13481378
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;
13491379
}
1350-
return symbol;
13511380
}
1381+
1382+
function symbolHasBeenSeen(symbol: Symbol) {
1383+
const symbolId = getSymbolId(symbol);
1384+
return !!symbolToOriginInfoMap[symbolId] || aliasesToAlreadyIncludedSymbols.has(symbolId.toString());
1385+
}
1386+
}
1387+
1388+
function getNearestExportSymbol(fromSymbol: Symbol) {
1389+
return forEachAlias(typeChecker, fromSymbol, alias => {
1390+
if (some(alias.declarations, d => isExportSpecifier(d) || !!d.localSymbol)) {
1391+
return alias;
1392+
}
1393+
});
13521394
}
13531395

13541396
/**

0 commit comments

Comments
 (0)