Skip to content

Commit c643493

Browse files
committed
changes from review
1 parent 2b905ce commit c643493

10 files changed

Lines changed: 80 additions & 56 deletions

File tree

packages/jsts/src/analysis/projectAnalysis/analyzeFile.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import ts from 'typescript';
3434
* Analyzes a single file, optionally with a TypeScript program for type-checking.
3535
* This is the common entry point for all analysis paths (with program, without program, with cache).
3636
*/
37-
export async function analyzeSingleFile(
37+
export async function analyzeFile(
3838
fileName: string,
3939
file: JsTsFiles[string],
4040
program: ts.Program | undefined,
@@ -53,7 +53,7 @@ export async function analyzeSingleFile(
5353
};
5454

5555
// Analyze the file (with error handling)
56-
const result = await analyzeFile(input);
56+
const result = await analyzeInput(input);
5757

5858
if (pendingFiles) {
5959
pendingFiles.delete(fileName);
@@ -65,7 +65,7 @@ export async function analyzeSingleFile(
6565
* Safely analyze a JavaScript/TypeScript file wrapping raised exceptions in the output format
6666
* @param input JsTsAnalysisInput object containing all the data necessary for the analysis
6767
*/
68-
async function analyzeFile(input: JsTsAnalysisInput): Promise<FileResult> {
68+
async function analyzeInput(input: JsTsAnalysisInput): Promise<FileResult> {
6969
try {
7070
return await getAnalyzerForFile(input.filePath)(input);
7171
} catch (e) {

packages/jsts/src/analysis/projectAnalysis/analyzeWithIncrementalProgram.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ProgressReport } from '../../../../shared/src/helpers/progress-report.j
2121
import { WsIncrementalResult } from '../../../../bridge/src/request.js';
2222
import { isAnalysisCancelled } from './analyzeProject.js';
2323
import { error, info, warn } from '../../../../shared/src/helpers/logging.js';
24-
import { analyzeSingleFile } from './analyzeFile.js';
24+
import { analyzeFile } from './analyzeFile.js';
2525
import { dirname } from 'node:path/posix';
2626
import { sanitizeReferences } from '../../program/tsconfig/utils.js';
2727
import ts from 'typescript';
@@ -68,7 +68,7 @@ export async function analyzeWithIncrementalProgram(
6868
programOptionsFromClosestTsconfig(filename, results, foundProgramOptions, pendingFiles),
6969
);
7070

71-
await analyzeSingleFile(
71+
await analyzeFile(
7272
filename,
7373
files[filename],
7474
program,
@@ -135,12 +135,14 @@ function programOptionsFromClosestTsconfig(
135135
try {
136136
info('No tsconfig found for files, using default options');
137137
// Fallback: use default options if no tsconfig found
138+
// TODO(JS-1138): File order can affect program combinations - improve strategy
138139
return createProgramOptionsFromJson(defaultCompilerOptions, [...pendingFiles], getBaseDir());
139140
} catch (e) {
140141
error(`Failed to generate program from merged config: ${e}`);
141142
}
142143
}
143144

145+
// TODO(JS-1139): Optimize by only checking tsconfigs in ancestor directories
144146
function pickBestMatchTsConfig(tsconfigs: string[], file: string) {
145147
let bestTsConfig: string | undefined = undefined;
146148
for (const tsconfig of tsconfigs) {

packages/jsts/src/analysis/projectAnalysis/analyzeWithProgram.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
1717
import type { JsTsFiles, ProjectAnalysisOutput } from './projectAnalysis.js';
18-
import { analyzeSingleFile } from './analyzeFile.js';
18+
import { analyzeFile } from './analyzeFile.js';
1919
import { error, info, warn } from '../../../../shared/src/helpers/logging.js';
2020
import { tsConfigStore } from './file-stores/index.js';
2121
import ts from 'typescript';
@@ -59,8 +59,9 @@ export async function analyzeWithProgram(
5959
const processedTSConfigs: Set<string> = new Set();
6060
const tsconfigs = tsConfigStore.getTsConfigs();
6161

62-
// Process tsconfigs, discovering references as we go
63-
// Iterator calls next() on each iteration, so newly added references are automatically processed
62+
// Process tsconfigs, discovering project references as we go.
63+
// When a tsconfig has project references, we add them via addDiscoveredTsConfig(),
64+
// and they will be included in this iteration since getTsConfigs() returns a live iterable.
6465
for (const tsConfig of tsconfigs) {
6566
if (isAnalysisCancelled()) {
6667
return;
@@ -98,7 +99,8 @@ export async function analyzeWithProgram(
9899
if (foundProgramOptions.some(options => options.missingTsConfig)) {
99100
results.meta.warnings.push(MISSING_EXTENDED_TSCONFIG);
100101
}
101-
// Clear caches after SonarQube analysis (single-run, don't persist)
102+
// Clear caches after SonarQube analysis to free memory (single-run, don't persist).
103+
// Note: analyzeWithIncrementalProgram (SonarLint) intentionally keeps caches for subsequent requests.
102104
const cacheManager = getProgramCacheManager();
103105
const stats = cacheManager.getCacheStats();
104106
if (stats.size > 0) {
@@ -140,7 +142,7 @@ async function analyzeFilesFromEntryPoint(
140142
return;
141143
}
142144

143-
await analyzeSingleFile(
145+
await analyzeFile(
144146
fileName,
145147
files[fileName],
146148
tsProgram,
@@ -213,7 +215,7 @@ async function analyzeFilesFromTsConfig(
213215
return;
214216
}
215217

216-
await analyzeSingleFile(
218+
await analyzeFile(
217219
fileName,
218220
files[fileName],
219221
tsProgram,

packages/jsts/src/analysis/projectAnalysis/analyzeWithoutProgram.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
1717
import { JsTsFiles, ProjectAnalysisOutput } from './projectAnalysis.js';
18-
import { getBaseDir } from '../../../../shared/src/helpers/configuration.js';
19-
import { debug } from '../../../../shared/src/helpers/logging.js';
18+
import { getBaseDir, isJsTsFile } from '../../../../shared/src/helpers/configuration.js';
19+
import { debug, warn } from '../../../../shared/src/helpers/logging.js';
2020
import { relative } from 'node:path/posix';
2121
import { ProgressReport } from '../../../../shared/src/helpers/progress-report.js';
2222
import { WsIncrementalResult } from '../../../../bridge/src/request.js';
2323
import { isAnalysisCancelled } from './analyzeProject.js';
24-
import { analyzeSingleFile } from './analyzeFile.js';
24+
import { analyzeFile } from './analyzeFile.js';
2525

2626
/**
2727
* Analyzes files without type-checking.
@@ -43,8 +43,15 @@ export async function analyzeWithoutProgram(
4343
if (isAnalysisCancelled()) {
4444
return;
4545
}
46-
debug(`File not part of any tsconfig.json: ${relative(getBaseDir(), filename)}`);
47-
await analyzeSingleFile(
46+
const relativePath = relative(getBaseDir(), filename);
47+
if (isJsTsFile(filename)) {
48+
warn(
49+
`JS/TS file analyzed without type-checking (not part of any tsconfig.json): ${relativePath}`,
50+
);
51+
} else {
52+
debug(`File not part of any tsconfig.json: ${relativePath}`);
53+
}
54+
await analyzeFile(
4855
filename,
4956
files[filename],
5057
undefined,

packages/jsts/src/program/cache/programOptionsCache.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,33 @@ import type { ProgramOptions } from '../tsconfig/options.js';
1919

2020
/**
2121
* Cache for createProgramOptions() results
22-
* Key: tsConfig path and hash of tsconfigContents (if provided)
22+
* Key: tsConfig path + tsconfigContents (if provided)
2323
*/
2424
const programOptionsCache = new Map<string, ProgramOptions>();
2525

26+
function buildCacheKey(tsConfig: string, tsconfigContents?: string): string {
27+
return `${tsConfig}:${tsconfigContents}`;
28+
}
29+
2630
/**
2731
* Get cached ProgramOptions from createProgramOptions()
2832
*/
29-
export function getCachedProgramOptions(cacheKey: string): ProgramOptions | undefined {
30-
return programOptionsCache.get(cacheKey);
33+
export function getCachedProgramOptions(
34+
tsConfig: string,
35+
tsconfigContents?: string,
36+
): ProgramOptions | undefined {
37+
return programOptionsCache.get(buildCacheKey(tsConfig, tsconfigContents));
3138
}
3239

3340
/**
3441
* Store ProgramOptions in cache for createProgramOptions()
3542
*/
36-
export function setCachedProgramOptions(cacheKey: string, options: ProgramOptions): void {
37-
programOptionsCache.set(cacheKey, options);
43+
export function setCachedProgramOptions(
44+
tsConfig: string,
45+
options: ProgramOptions,
46+
tsconfigContents?: string,
47+
): void {
48+
programOptionsCache.set(buildCacheKey(tsConfig, tsconfigContents), options);
3849
}
3950

4051
/**

packages/jsts/src/program/cache/sourceFileCache.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ 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, contentHash).
29+
* This cache stores parsed ASTs keyed by (fileName, scriptTarget).
30+
* The contentHash is stored as a value to validate cache freshness.
3031
* Multiple programs can share the same parsed AST if they have the same target.
3132
*
3233
* IMPLEMENTATION NOTE: We cache SourceFiles per ScriptTarget to preserve the
@@ -95,6 +96,9 @@ export function getCachedSourceFile(
9596
}
9697

9798
const cached = targetCache.get(scriptTarget);
99+
// If hash doesn't match, return undefined (cache miss). The caller will parse
100+
// the file and call setCachedSourceFile, which overwrites the stale entry.
101+
// This makes the cache self-healing - stale entries cause one miss, then get fixed.
98102
if (cached?.contentHash === contentHash) {
99103
return cached.sourceFile;
100104
}
@@ -125,11 +129,11 @@ export function setCachedSourceFile(
125129
}
126130

127131
/**
128-
* Invalidate cached SourceFile for a specific file (all targets)
132+
* Invalidate parsed SourceFile AST cache for a specific file (all targets)
129133
* Used when file content changes
130134
* @param fileName Normalized file path
131135
*/
132-
export function invalidateCachedSourceFile(fileName: string): void {
136+
export function invalidateParsedSourceFile(fileName: string): void {
133137
parsedSourceFileCache.delete(fileName);
134138
}
135139

packages/jsts/src/program/compilerHost.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
getCurrentFilesContext,
2222
getCachedSourceFile,
2323
setCachedSourceFile,
24-
invalidateCachedSourceFile,
24+
invalidateParsedSourceFile,
2525
} from './cache/sourceFileCache.js';
2626

2727
interface FsCall {
@@ -38,6 +38,8 @@ interface FsCall {
3838
* - Incremental file updates for builder programs
3939
*/
4040
export class IncrementalCompilerHost implements ts.CompilerHost {
41+
// Map from normalized file path to version number (incremented on file changes).
42+
// Used by TypeScript's builder programs for change detection.
4143
private readonly fileVersions = new Map<string, number>();
4244
private readonly modifiedFiles = new Set<string>();
4345
private fsCallTracker: FsCall[] = [];
@@ -71,7 +73,7 @@ export class IncrementalCompilerHost implements ts.CompilerHost {
7173
this.modifiedFiles.add(normalized);
7274

7375
// Invalidate global parsed SourceFile cache for this file (all targets)
74-
invalidateCachedSourceFile(normalized);
76+
invalidateParsedSourceFile(normalized);
7577

7678
return true; // File was updated
7779
}
@@ -141,7 +143,9 @@ export class IncrementalCompilerHost implements ts.CompilerHost {
141143
if (content) {
142144
cache.set(normalized, content);
143145

144-
// Update files object if entry exists (keep cache and files in sync)
146+
// Update files object if entry already exists (keep cache and files in sync).
147+
// We only update existing entries - we don't create new ones as the filesContext
148+
// represents files explicitly provided by the caller.
145149
if (filesContext?.[fileName]) {
146150
filesContext[fileName].fileContent = content;
147151
}
@@ -167,6 +171,7 @@ export class IncrementalCompilerHost implements ts.CompilerHost {
167171
}
168172

169173
// 3. Fallback to filesystem
174+
// TODO: Consider caching negative results to avoid repeated filesystem checks for missing files
170175
this.trackFsCall('fileExists-disk', fileName);
171176
return this.baseHost.fileExists(fileName);
172177
}
@@ -200,8 +205,6 @@ export class IncrementalCompilerHost implements ts.CompilerHost {
200205
const content = this.readFile(fileName);
201206
let sourceFile: (ts.SourceFile & { version?: string }) | undefined;
202207
if (content) {
203-
this.trackFsCall('getSourceFile', fileName);
204-
205208
// Parse the file
206209
sourceFile = ts.createSourceFile(
207210
fileName,
@@ -213,7 +216,8 @@ export class IncrementalCompilerHost implements ts.CompilerHost {
213216
// Store in global cache for reuse across programs
214217
setCachedSourceFile(normalized, scriptTarget, contentHash, sourceFile);
215218
} else {
216-
// Fallback to base host for files we don't have
219+
// Fallback to base host for files not in our cache/context (e.g., TypeScript lib files).
220+
// readFile returns undefined for these, but baseHost can resolve them.
217221
this.trackFsCall('getSourceFile-disk-fallback', fileName);
218222
sourceFile = this.baseHost.getSourceFile(
219223
fileName,

packages/jsts/src/program/tsconfig/options.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function createProgramOptionsFromJson(
124124
*/
125125
export function createProgramOptions(tsConfig: string, tsconfigContents?: string): ProgramOptions {
126126
// Check cache first
127-
const cached = getCachedProgramOptions(`${tsConfig}:${tsconfigContents}`);
127+
const cached = getCachedProgramOptions(tsConfig, tsconfigContents);
128128
if (cached) {
129129
return cached;
130130
}
@@ -157,7 +157,7 @@ export function createProgramOptions(tsConfig: string, tsconfigContents?: string
157157
return cachedTsConfig.contents;
158158
}
159159

160-
// 3. Read from disk
160+
// 3. Read from filesystem or sourceFileStore (depending on canAccessFileSystem())
161161
const fileContents = defaultParseConfigHost.readFile(file);
162162

163163
// 4. Handle missing extended tsconfig (return empty config)
@@ -226,7 +226,7 @@ export function createProgramOptions(tsConfig: string, tsconfigContents?: string
226226
[PROGRAM_OPTIONS_BRAND]: true,
227227
} as const;
228228

229-
setCachedProgramOptions(`${tsConfig}:${tsconfigContents}`, result);
229+
setCachedProgramOptions(tsConfig, result, tsconfigContents);
230230

231231
return result;
232232
}

packages/jsts/tests/program/programOptionsCache.test.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,16 @@ describe('programOptionsCache', () => {
3636

3737
describe('getCachedProgramOptions', () => {
3838
it('should return undefined for cache miss', () => {
39-
const result = getCachedProgramOptions('/project/tsconfig.json:undefined');
39+
const result = getCachedProgramOptions('/project/tsconfig.json');
4040
expect(result).toBeUndefined();
4141
});
4242

4343
it('should return cached options for cache hit', () => {
4444
const tsConfig = path.join(fixtures, 'tsconfig.json');
4545
const options = createProgramOptions(tsConfig);
46-
const cacheKey = `${tsConfig}:custom`;
4746

48-
setCachedProgramOptions(cacheKey, options);
49-
const result = getCachedProgramOptions(cacheKey);
47+
setCachedProgramOptions(tsConfig, options, 'custom');
48+
const result = getCachedProgramOptions(tsConfig, 'custom');
5049

5150
expect(result).toBe(options);
5251
});
@@ -56,11 +55,10 @@ describe('programOptionsCache', () => {
5655
it('should store options in cache', () => {
5756
const tsConfig = path.join(fixtures, 'tsconfig.json');
5857
const options = createProgramOptions(tsConfig);
59-
const cacheKey = `${tsConfig}:custom`;
6058

61-
setCachedProgramOptions(cacheKey, options);
59+
setCachedProgramOptions(tsConfig, options, 'custom');
6260

63-
expect(getCachedProgramOptions(cacheKey)).toBe(options);
61+
expect(getCachedProgramOptions(tsConfig, 'custom')).toBe(options);
6462
});
6563

6664
it('should handle different cache keys for same tsconfig with different contents', () => {
@@ -72,14 +70,11 @@ describe('programOptionsCache', () => {
7270

7371
const options2 = createProgramOptions(tsConfig);
7472

75-
const cacheKey1 = `${tsConfig}:content1`;
76-
const cacheKey2 = `${tsConfig}:content2`;
73+
setCachedProgramOptions(tsConfig, options1, 'content1');
74+
setCachedProgramOptions(tsConfig, options2, 'content2');
7775

78-
setCachedProgramOptions(cacheKey1, options1);
79-
setCachedProgramOptions(cacheKey2, options2);
80-
81-
expect(getCachedProgramOptions(cacheKey1)).toBe(options1);
82-
expect(getCachedProgramOptions(cacheKey2)).toBe(options2);
76+
expect(getCachedProgramOptions(tsConfig, 'content1')).toBe(options1);
77+
expect(getCachedProgramOptions(tsConfig, 'content2')).toBe(options2);
8378
});
8479

8580
it('should overwrite existing cache entry', () => {
@@ -91,12 +86,11 @@ describe('programOptionsCache', () => {
9186
clearTsConfigContentCache();
9287

9388
const options2 = createProgramOptions(tsConfig2);
94-
const cacheKey = 'shared-key';
9589

96-
setCachedProgramOptions(cacheKey, options1);
97-
setCachedProgramOptions(cacheKey, options2);
90+
setCachedProgramOptions('shared-key', options1);
91+
setCachedProgramOptions('shared-key', options2);
9892

99-
expect(getCachedProgramOptions(cacheKey)).toBe(options2);
93+
expect(getCachedProgramOptions('shared-key')).toBe(options2);
10094
});
10195
});
10296

0 commit comments

Comments
 (0)