@@ -1255,23 +1255,60 @@ namespace ts.Completions {
1255
1255
typeChecker . getExportsOfModule ( sym ) . some ( e => symbolCanBeReferencedAtTypeLocation ( e , seenModules ) ) ;
1256
1256
}
1257
1257
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 {
1259
1306
const tokenTextLowerCase = tokenText . toLowerCase ( ) ;
1260
1307
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 } > ( ) ;
1275
1312
1276
1313
codefix . forEachExternalModuleToImportFrom ( typeChecker , host , preferences , program . redirectTargetsMap , sourceFile , program . getSourceFiles ( ) , moduleSymbol => {
1277
1314
// Perf -- ignore other modules if this is a request for details
@@ -1295,10 +1332,6 @@ namespace ts.Completions {
1295
1332
}
1296
1333
1297
1334
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
- //
1302
1335
// If `symbol.parent !== ...`, this is an `export * from "foo"` re-export. Those don't create new symbols.
1303
1336
if ( typeChecker . getMergedSymbol ( symbol . parent ! ) !== resolvedModuleSymbol ) {
1304
1337
continue ;
@@ -1309,32 +1342,29 @@ namespace ts.Completions {
1309
1342
}
1310
1343
// 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).
1311
1344
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 ) ) ;
1323
1355
}
1324
- continue ;
1325
1356
}
1326
1357
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 ) ;
1329
1361
}
1330
-
1331
- pushSymbol ( symbol , moduleSymbol ) ;
1332
1362
}
1333
1363
} ) ;
1334
1364
1335
1365
// 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 ) ) ;
1338
1368
1339
1369
function pushSymbol ( symbol : Symbol , moduleSymbol : Symbol , insertAt = symbols . length ) {
1340
1370
const isDefaultExport = symbol . escapedName === InternalSymbolName . Default ;
@@ -1347,8 +1377,20 @@ namespace ts.Completions {
1347
1377
symbolToSortTextMap [ getSymbolId ( symbol ) ] = SortText . AutoImportSuggestions ;
1348
1378
symbolToOriginInfoMap [ getSymbolId ( symbol ) ] = origin ;
1349
1379
}
1350
- return symbol ;
1351
1380
}
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
+ } ) ;
1352
1394
}
1353
1395
1354
1396
/**
0 commit comments