@@ -26,32 +26,32 @@ const sourceFileContentCache = new Map<string, string>();
2626/**
2727 * Global cache for parsed TypeScript SourceFile ASTs
2828 *
29- * This cache stores parsed ASTs keyed by (fileName, scriptTarget).
29+ * This cache stores parsed ASTs keyed by (fileName, scriptTarget, jsx, importHelpers ).
3030 * The contentHash is stored as a value to validate cache freshness.
31- * Multiple programs can share the same parsed AST if they have the same target .
31+ * Multiple programs can share the same parsed AST if they have the same options .
3232 *
33- * IMPLEMENTATION NOTE: We cache SourceFiles per ScriptTarget to preserve the
34- * languageVersion metadata stored in the SourceFile object. While our testing shows
35- * that the AST structure is identical across targets and type checking uses the
36- * program's target (not the SourceFile's languageVersion), we keep target-specific
37- * caching for these reasons:
38- *
39- * 1. Performance gain of target-agnostic caching may be marginal since most analysis
40- * runs use the same target configuration
41- * 2. Preserves metadata integrity - SourceFile.languageVersion matches the intended target
42- * 3. Future-proofing - allows filtering rules based on configured target if needed
43- * 4. TypeScript API contract - creating SourceFiles with their intended target is the
44- * documented usage pattern
45- *
46- * If profiling shows that different targets are commonly used and parsing becomes a
47- * bottleneck, we could simplify to target-agnostic caching (always use ESNext) with
48- * minimal impact on functionality.
33+ * IMPLEMENTATION NOTE: We key on scriptTarget, jsx, and importHelpers because all
34+ * three affect the structure of SourceFile.imports (the synthesized JSX runtime import
35+ * prepended by TypeScript when jsx=react-jsx, and tslib helpers when importHelpers=true).
36+ * Sharing a SourceFile between programs with different values for these options causes
37+ * internal TypeScript assertion failures (e.g. in getSuggestionDiagnostics).
4938 */
5039const parsedSourceFileCache = new Map <
5140 string , // normalized file path
52- Map < ts . ScriptTarget , { contentHash : string ; sourceFile : ts . SourceFile } >
41+ Map < string , { contentHash : string ; sourceFile : ts . SourceFile } > // compound key → entry
5342> ( ) ;
5443
44+ /**
45+ * Build a compound cache key from the options that affect SourceFile.imports structure.
46+ */
47+ function makeParsedSourceFileCacheKey (
48+ scriptTarget : ts . ScriptTarget ,
49+ jsx : ts . JsxEmit | undefined ,
50+ importHelpers = false ,
51+ ) : string {
52+ return `${ scriptTarget } :${ jsx ?? - 1 } :${ importHelpers ? 1 : 0 } ` ;
53+ }
54+
5555/**
5656 * Current files context for lazy loading
5757 */
@@ -79,23 +79,28 @@ export function getCurrentFilesContext(): Record<string, { fileContent?: string
7979}
8080
8181/**
82- * Get a cached parsed SourceFile for a given file and target
82+ * Get a cached parsed SourceFile for a given file and compiler options
8383 * @param fileName Normalized file path
8484 * @param scriptTarget TypeScript ScriptTarget (e.g., ES5, ES2020, ESNext)
8585 * @param contentHash Hash/version of the file content
86+ * @param jsx The jsx compiler option (affects SourceFile.imports structure)
87+ * @param importHelpers The importHelpers compiler option (affects SourceFile.imports structure)
8688 * @returns Cached SourceFile if available and content matches, undefined otherwise
8789 */
8890export function getCachedSourceFile (
8991 fileName : string ,
9092 scriptTarget : ts . ScriptTarget ,
9193 contentHash : string ,
94+ jsx ?: ts . JsxEmit ,
95+ importHelpers = false ,
9296) : ts . SourceFile | undefined {
93- const targetCache = parsedSourceFileCache . get ( fileName ) ;
94- if ( ! targetCache ) {
97+ const fileCache = parsedSourceFileCache . get ( fileName ) ;
98+ if ( ! fileCache ) {
9599 return undefined ;
96100 }
97101
98- const cached = targetCache . get ( scriptTarget ) ;
102+ const key = makeParsedSourceFileCacheKey ( scriptTarget , jsx , importHelpers ) ;
103+ const cached = fileCache . get ( key ) ;
99104 // If hash doesn't match, return undefined (cache miss). The caller will parse
100105 // the file and call setCachedSourceFile, which overwrites the stale entry.
101106 // This makes the cache self-healing - stale entries cause one miss, then get fixed.
@@ -112,20 +117,25 @@ export function getCachedSourceFile(
112117 * @param scriptTarget TypeScript ScriptTarget used to parse the file
113118 * @param contentHash Hash/version of the file content
114119 * @param sourceFile The parsed TypeScript SourceFile
120+ * @param jsx The jsx compiler option (affects SourceFile.imports structure)
121+ * @param importHelpers The importHelpers compiler option (affects SourceFile.imports structure)
115122 */
116123export function setCachedSourceFile (
117124 fileName : string ,
118125 scriptTarget : ts . ScriptTarget ,
119126 contentHash : string ,
120127 sourceFile : ts . SourceFile ,
128+ jsx ?: ts . JsxEmit ,
129+ importHelpers = false ,
121130) : void {
122- let targetCache = parsedSourceFileCache . get ( fileName ) ;
123- if ( ! targetCache ) {
124- targetCache = new Map ( ) ;
125- parsedSourceFileCache . set ( fileName , targetCache ) ;
131+ let fileCache = parsedSourceFileCache . get ( fileName ) ;
132+ if ( ! fileCache ) {
133+ fileCache = new Map ( ) ;
134+ parsedSourceFileCache . set ( fileName , fileCache ) ;
126135 }
127136
128- targetCache . set ( scriptTarget , { contentHash, sourceFile } ) ;
137+ const key = makeParsedSourceFileCacheKey ( scriptTarget , jsx , importHelpers ) ;
138+ fileCache . set ( key , { contentHash, sourceFile } ) ;
129139}
130140
131141/**
0 commit comments