Skip to content

Commit 9c57339

Browse files
zgliczclaude
andauthored
JS-1340 Add sonar.javascript.createTSProgramForOrphanFiles flag (#6411)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1c8021f commit 9c57339

File tree

8 files changed

+129
-10
lines changed

8 files changed

+129
-10
lines changed

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,22 @@ export async function analyzeWithProgram(
9797
);
9898
}
9999

100-
await analyzeFilesFromEntryPoint(
101-
files,
102-
results,
103-
pendingFiles,
104-
foundProgramOptions,
105-
progressReport,
106-
baseDir,
107-
jsTsConfigFields,
108-
incrementalResultsChannel,
109-
);
100+
if (jsTsConfigFields.createTSProgramForOrphanFiles) {
101+
await analyzeFilesFromEntryPoint(
102+
files,
103+
results,
104+
pendingFiles,
105+
foundProgramOptions,
106+
progressReport,
107+
baseDir,
108+
jsTsConfigFields,
109+
incrementalResultsChannel,
110+
);
111+
} else if (pendingFiles.size) {
112+
info(
113+
`Skipping TypeScript program creation for ${pendingFiles.size} orphan file(s) (sonar.javascript.createTSProgramForOrphanFiles=false)`,
114+
);
115+
}
110116

111117
if (foundProgramOptions.some(options => options.missingTsConfig)) {
112118
results.meta.warnings.push(MISSING_EXTENDED_TSCONFIG);

packages/jsts/tests/analysis/analyzeProject-sonarqube.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,70 @@ describe('SonarQube project analysis', () => {
489489
expect('issues' in fileResult! && fileResult!.issues.length).toBeGreaterThan(0);
490490
});
491491

492+
it('should skip TypeScript program creation for orphan files when createTSProgramForOrphanFiles is false', async () => {
493+
const baseDir = join(fixtures, 'no-tsconfig');
494+
const filePath = join(baseDir, 'orphan.ts');
495+
496+
console.log = mock.fn(console.log);
497+
const consoleLogMock = (console.log as Mock<typeof console.log>).mock;
498+
499+
const configuration = await initForTest(
500+
{ baseDir, createTSProgramForOrphanFiles: false },
501+
{ [filePath]: { filePath, fileType: 'MAIN' } },
502+
);
503+
504+
const result = await analyzeProject({ rules, bundles: [] }, configuration);
505+
506+
// Should log the skipping message
507+
expect(
508+
consoleLogMock.calls.some(call =>
509+
(call.arguments[0] as string)?.includes('Skipping TypeScript program creation for'),
510+
),
511+
).toBe(true);
512+
513+
// The file should still be analyzed (without type info, via analyzeWithoutProgram)
514+
expect(result.files[normalizeToAbsolutePath(filePath)]).toBeDefined();
515+
516+
// Should NOT log "using default options" since we skip the entry point program creation
517+
expect(
518+
consoleLogMock.calls.some(call =>
519+
(call.arguments[0] as string)?.includes('using default options'),
520+
),
521+
).toBe(false);
522+
});
523+
524+
it('should create TypeScript program for orphan files when createTSProgramForOrphanFiles is true', async () => {
525+
const baseDir = join(fixtures, 'no-tsconfig');
526+
const filePath = join(baseDir, 'orphan.ts');
527+
528+
console.log = mock.fn(console.log);
529+
const consoleLogMock = (console.log as Mock<typeof console.log>).mock;
530+
531+
const configuration = await initForTest(
532+
{ baseDir, createTSProgramForOrphanFiles: true },
533+
{ [filePath]: { filePath, fileType: 'MAIN' } },
534+
);
535+
536+
const result = await analyzeProject({ rules, bundles: [] }, configuration);
537+
538+
// Should log the default options message (entry point analysis runs)
539+
expect(
540+
consoleLogMock.calls.some(call =>
541+
(call.arguments[0] as string)?.includes('using default options'),
542+
),
543+
).toBe(true);
544+
545+
// Should NOT log the skipping message
546+
expect(
547+
consoleLogMock.calls.some(call =>
548+
(call.arguments[0] as string)?.includes('Skipping TypeScript program creation for'),
549+
),
550+
).toBe(false);
551+
552+
// File should be analyzed
553+
expect(result.files[normalizeToAbsolutePath(filePath)]).toBeDefined();
554+
});
555+
492556
it('should report parsing errors', async () => {
493557
const baseDir = join(fixtures, 'parsing-error');
494558
const filePath = join(baseDir, 'file.js');

packages/shared/src/helpers/configuration.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export type Configuration = {
7373
testInclusions: Minimatch[] /* sonar.test.inclusions - WILDCARD to narrow down sonar.tests */;
7474
testExclusions: Minimatch[] /* sonar.test.exclusions - WILDCARD to narrow down sonar.tests */;
7575
detectBundles: boolean /* sonar.javascript.detectBundles - whether files looking like bundled code should be ignored */;
76+
createTSProgramForOrphanFiles: boolean /* sonar.javascript.createTSProgramForOrphanFiles - whether to create a TS program for orphan files */;
7677
};
7778

7879
// Patterns enforced to be ignored no matter what the user configures on sonar.properties
@@ -206,6 +207,9 @@ export function createConfiguration(raw: unknown): Configuration {
206207
testInclusions: normalizeGlobs(raw.testInclusions, baseDir),
207208
testExclusions: normalizeGlobs(raw.testExclusions, baseDir),
208209
detectBundles: isBoolean(raw.detectBundles) ? raw.detectBundles : true,
210+
createTSProgramForOrphanFiles: isBoolean(raw.createTSProgramForOrphanFiles)
211+
? raw.createTSProgramForOrphanFiles
212+
: true,
209213
};
210214
}
211215

@@ -348,6 +352,7 @@ export type JsTsConfigFields = {
348352
skipAst: boolean;
349353
sonarlint: boolean;
350354
shouldIgnoreParams: ShouldIgnoreFileParams;
355+
createTSProgramForOrphanFiles: boolean;
351356
};
352357

353358
/**
@@ -365,5 +370,6 @@ export function getJsTsConfigFields(configuration: Configuration): JsTsConfigFie
365370
skipAst: configuration.skipAst,
366371
sonarlint: configuration.sonarlint,
367372
shouldIgnoreParams: getShouldIgnoreParams(configuration),
373+
createTSProgramForOrphanFiles: configuration.createTSProgramForOrphanFiles,
368374
};
369375
}

sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/AnalysisConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public interface AnalysisConfiguration {
4949

5050
boolean canAccessFileSystem();
5151

52+
boolean shouldCreateTSProgramForOrphanFiles();
53+
5254
List<String> getSources();
5355

5456
List<String> getInclusions();

sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class ProjectAnalysisConfiguration {
193193
List<String> testExclusions;
194194
boolean detectBundles;
195195
boolean canAccessFileSystem;
196+
boolean createTSProgramForOrphanFiles;
196197

197198
/*
198199
We do not set sources, inclusions, exclusions, tests, testInclusions nor testExclusions as Sonar Engine
@@ -227,6 +228,8 @@ public ProjectAnalysisConfiguration(
227228
this.jsTsExclusions = analysisConfiguration.getJsTsExcludedPaths();
228229
this.detectBundles = analysisConfiguration.shouldDetectBundles();
229230
this.canAccessFileSystem = analysisConfiguration.canAccessFileSystem();
231+
this.createTSProgramForOrphanFiles =
232+
analysisConfiguration.shouldCreateTSProgramForOrphanFiles();
230233
}
231234

232235
public boolean skipAst() {

sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public class JavaScriptPlugin implements Plugin {
135135
public static final String SKIP_NODE_PROVISIONING_PROPERTY = "sonar.scanner.skipNodeProvisioning";
136136
public static final String DETECT_BUNDLES_PROPERTY = "sonar.javascript.detectBundles";
137137
public static final String NO_FS = "sonar.javascript.canAccessFileSystem";
138+
public static final String CREATE_TS_PROGRAM_FOR_ORPHAN_FILES =
139+
"sonar.javascript.createTSProgramForOrphanFiles";
138140

139141
@Override
140142
public void define(Context context) {
@@ -254,6 +256,18 @@ public void define(Context context) {
254256
.subCategory(GENERAL)
255257
.category(JS_TS_CATEGORY)
256258
.type(PropertyType.BOOLEAN)
259+
.build(),
260+
PropertyDefinition.builder(CREATE_TS_PROGRAM_FOR_ORPHAN_FILES)
261+
.defaultValue("true")
262+
.name("Create TypeScript program for orphan files")
263+
.description(
264+
"Controls whether a TypeScript program should be created for files not included in any tsconfig.json. " +
265+
"When disabled, orphan files are analyzed without type information, which is faster but may reduce analysis accuracy."
266+
)
267+
.onConfigScopes(PropertyDefinition.ConfigScope.PROJECT)
268+
.subCategory(TS_SUB_CATEGORY)
269+
.category(JS_TS_CATEGORY)
270+
.type(PropertyType.BOOLEAN)
257271
.build()
258272
);
259273

sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/JsTsContext.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ public static boolean canAccessFileSystem(Configuration config) {
230230
return config.getBoolean(JavaScriptPlugin.NO_FS).orElse(true);
231231
}
232232

233+
public boolean shouldCreateTSProgramForOrphanFiles() {
234+
return context
235+
.config()
236+
.getBoolean(JavaScriptPlugin.CREATE_TS_PROGRAM_FOR_ORPHAN_FILES)
237+
.orElse(true);
238+
}
239+
233240
public List<String> getSources() {
234241
return stream(this.context.config().getStringArray("sonar.sources"))
235242
.filter(x -> !x.isBlank())

sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,23 @@ void skipNodeProvisioningPropertyIsCorrectlyExposed() {
121121
assertThat(propertyDefinition.subCategory()).isEqualTo("General");
122122
}
123123

124+
@Test
125+
void createTSProgramForOrphanFilesPropertyIsCorrectlyExposed() {
126+
var propertyDefinition = properties()
127+
.stream()
128+
.filter(item -> {
129+
return Objects.equals(item.key(), "sonar.javascript.createTSProgramForOrphanFiles");
130+
})
131+
.findFirst()
132+
.get();
133+
134+
assertThat(propertyDefinition.name()).isEqualTo("Create TypeScript program for orphan files");
135+
assertThat(propertyDefinition.type().toString()).isEqualTo("BOOLEAN");
136+
assertThat(propertyDefinition.defaultValue()).isEqualTo("true");
137+
assertThat(propertyDefinition.category()).isEqualTo("JavaScript / TypeScript");
138+
assertThat(propertyDefinition.subCategory()).isEqualTo("TypeScript");
139+
}
140+
124141
private List<PropertyDefinition> properties() {
125142
var extensions = setupContext(
126143
SonarRuntimeImpl.forSonarQube(LTS_VERSION, SonarQubeSide.SERVER, SonarEdition.COMMUNITY)

0 commit comments

Comments
 (0)