-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Store symbol table map key in CachedSymbolExportInfo #45289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3289,6 +3289,10 @@ namespace ts { | |
| return startsWith(symbol.escapedName as string, "__@"); | ||
| } | ||
|
|
||
| export function isPrivateIdentifierSymbol(symbol: Symbol): boolean { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm. I had to figure this out recently and I thought the Right Way was
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would readily believe, or at least hope, that those are equivalent, but this implementation gets to the core of what went wrong in the linked crash. You cannot ask for a name starting with |
||
| return startsWith(symbol.escapedName as string, "__#"); | ||
| } | ||
|
|
||
| /** | ||
| * Includes the word "Symbol" with unicode escapes | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ namespace ts { | |
| // Used to rehydrate `symbol` and `moduleSymbol` when transient | ||
| id: number; | ||
| symbolName: string; | ||
| symbolTableKey: __String; | ||
| moduleName: string; | ||
| moduleFile: SourceFile | undefined; | ||
|
|
||
|
|
@@ -44,7 +45,7 @@ namespace ts { | |
| export interface ExportInfoMap { | ||
| isUsableByFile(importingFile: Path): boolean; | ||
| clear(): void; | ||
| add(importingFile: Path, symbol: Symbol, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void; | ||
| add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void; | ||
| get(importingFile: Path, importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker): readonly SymbolExportInfo[] | undefined; | ||
| forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean) => void): void; | ||
| releaseSymbols(): void; | ||
|
|
@@ -71,13 +72,19 @@ namespace ts { | |
| symbols.clear(); | ||
| usableByFileName = undefined; | ||
| }, | ||
| add: (importingFile, symbol, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => { | ||
| add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => { | ||
| if (importingFile !== usableByFileName) { | ||
| cache.clear(); | ||
| usableByFileName = importingFile; | ||
| } | ||
| const isDefault = exportKind === ExportKind.Default; | ||
| const importedName = getNameForExportedSymbol(isDefault && getLocalSymbolForExportDefault(symbol) || symbol, scriptTarget); | ||
| const namedSymbol = isDefault && getLocalSymbolForExportDefault(symbol) || symbol; | ||
| // A re-export merged with an export from a module augmentation can result in `symbol` | ||
| // being an external module symbol; the name it is re-exported by will be `symbolTableKey` | ||
| // (which comes from the keys of `moduleSymbol.exports`.) | ||
| const importedName = isExternalModuleSymbol(namedSymbol) | ||
| ? unescapeLeadingUnderscores(symbolTableKey) | ||
| : getNameForExportedSymbol(namedSymbol, scriptTarget); | ||
| const moduleName = stripQuotes(moduleSymbol.name); | ||
| const id = exportInfoId++; | ||
| const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol; | ||
|
|
@@ -86,6 +93,7 @@ namespace ts { | |
|
|
||
| exportInfo.add(key(importedName, symbol, moduleName, checker), { | ||
| id, | ||
| symbolTableKey, | ||
| symbolName: importedName, | ||
| moduleName, | ||
| moduleFile, | ||
|
|
@@ -160,12 +168,10 @@ namespace ts { | |
| const moduleSymbol = info.moduleSymbol || cachedModuleSymbol || Debug.checkDefined(info.moduleFile | ||
| ? checker.getMergedSymbol(info.moduleFile.symbol) | ||
| : checker.tryFindAmbientModule(info.moduleName)); | ||
| const symbolName = exportKind === ExportKind.Default | ||
| ? InternalSymbolName.Default | ||
| : info.symbolName; | ||
| const symbol = info.symbol || cachedSymbol || Debug.checkDefined(exportKind === ExportKind.ExportEquals | ||
| ? checker.resolveExternalModuleSymbol(moduleSymbol) | ||
| : checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol)); | ||
| : checker.tryGetMemberInModuleExportsAndProperties(unescapeLeadingUnderscores(info.symbolTableKey), moduleSymbol), | ||
| `Could not find symbol '${info.symbolName}' by key '${info.symbolTableKey}' in module ${moduleSymbol.name}`); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Double checking with @amcasey that this is ok to put in an error message.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Obviously, the less customer content, the better but, as we've discussed, we have reason to believe this will be broadly useful and the telemetry for the resulting exception will be sanitized appropriately. |
||
| symbols.set(id, [symbol, moduleSymbol]); | ||
| return { | ||
| symbol, | ||
|
|
@@ -330,30 +336,32 @@ namespace ts { | |
| const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); | ||
| // Note: I think we shouldn't actually see resolved module symbols here, but weird merges | ||
| // can cause it to happen: see 'completionsImport_mergedReExport.ts' | ||
| if (defaultInfo && !checker.isUndefinedSymbol(defaultInfo.symbol) && !isExternalModuleSymbol(defaultInfo.symbol)) { | ||
| if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) { | ||
| cache.add( | ||
| importingFile.path, | ||
| defaultInfo.symbol, | ||
| defaultInfo.exportKind === ExportKind.Default ? InternalSymbolName.Default : InternalSymbolName.ExportEquals, | ||
| moduleSymbol, | ||
| moduleFile, | ||
| defaultInfo.exportKind, | ||
| isFromPackageJson, | ||
| scriptTarget, | ||
| checker); | ||
| } | ||
| for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { | ||
| if (exported !== defaultInfo?.symbol && !isKnownSymbol(exported) && !isExternalModuleSymbol(exported) && addToSeen(seenExports, exported)) { | ||
| checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => { | ||
| if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, exported)) { | ||
| cache.add( | ||
| importingFile.path, | ||
| exported, | ||
| key, | ||
| moduleSymbol, | ||
| moduleFile, | ||
| ExportKind.Named, | ||
| isFromPackageJson, | ||
| scriptTarget, | ||
| checker); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| host.log?.(`getExportInfoMap: done in ${timestamp() - start} ms`); | ||
|
|
@@ -368,6 +376,10 @@ namespace ts { | |
| return info && { symbol, exportKind, ...info }; | ||
| } | ||
|
|
||
| function isImportableSymbol(symbol: Symbol, checker: TypeChecker) { | ||
| return !checker.isUndefinedSymbol(symbol) && !checker.isUnknownSymbol(symbol) && !isKnownSymbol(symbol) && !isPrivateIdentifierSymbol(symbol); | ||
| } | ||
|
|
||
| function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined { | ||
| const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); | ||
| if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have had to refactor or duplicate a lot of checker code to get the actual map key of
symbol.membershere, so I’m hoping that the map key for a property is alwayssymbol.escapedName, which is not always true for an export. I added a debug assertion here while running tests and it never triggered, but I don’t want to leave it in production code because this is an extremely hot path (can easily hit tens of thousands of times while building a large export info map).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the penalty for providing the wrong key in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will crash in
rehydrateCachedInfo, for the same reason it does before this PR—no change. The cases I’ve identified wheresymbol.escapedNamewas not the key were all inexports, notmembers, which definitely gets the real map key.