Skip to content

Commit bc5cd72

Browse files
committed
JS-1453 Address PR feedback and add telemetry normalization tests
1 parent 4b16ff0 commit bc5cd72

File tree

5 files changed

+96
-38
lines changed

5 files changed

+96
-38
lines changed

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,7 @@ async function analyzeFilesFromEntryPoint(
177177
programOptions.host = new IncrementalCompilerHost(programOptions.options, baseDir);
178178

179179
telemetry.recordProgramCreationAttempt();
180-
const tsProgram = createStandardProgram(programOptions, {
181-
onProgramCreated: () => telemetry.recordProgramCreationSuccess(),
182-
onProgramCreationError: () => telemetry.recordProgramCreationFailure(),
183-
});
180+
const tsProgram = createStandardProgram(programOptions);
184181
const detectedEsYear = esLibToYear(programOptions.options.lib);
185182
telemetry.recordEcmaScriptVersion(detectedEsYear ?? undefined);
186183

@@ -251,10 +248,7 @@ async function analyzeFilesFromTsConfig(
251248
`Creating TypeScript(${ts.version}) program with configuration file ${tsconfig} [lib: ${programOptions.options.lib?.join(', ')}]`,
252249
);
253250
programOptions.host = new IncrementalCompilerHost(programOptions.options, baseDir);
254-
const tsProgram = createStandardProgram(programOptions, {
255-
onProgramCreated: () => telemetry.recordProgramCreationSuccess(),
256-
onProgramCreationError: () => telemetry.recordProgramCreationFailure(),
257-
});
251+
const tsProgram = createStandardProgram(programOptions);
258252
const detectedEsYear = esLibToYear(programOptions.options.lib);
259253
telemetry.recordEcmaScriptVersion(detectedEsYear ?? undefined);
260254

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

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export function getProjectAnalysisTelemetryCollector() {
5353
return projectAnalysisTelemetryCollector;
5454
}
5555

56+
export function getOptionalProjectAnalysisTelemetryCollector() {
57+
return projectAnalysisTelemetryCollector;
58+
}
59+
5660
export function getProjectAnalysisTelemetry(): ProjectAnalysisTelemetry {
5761
return getProjectAnalysisTelemetryCollector().getTelemetry();
5862
}
@@ -108,7 +112,7 @@ export class ProjectAnalysisTelemetryCollector {
108112
getTelemetry(): ProjectAnalysisTelemetry {
109113
const compilerOptions: Record<string, string[]> = {};
110114
for (const [optionName, values] of this.compilerOptionValues.entries()) {
111-
const sorted = Array.from(values).sort();
115+
const sorted = Array.from(values).sort((a, b) => a.localeCompare(b));
112116
if (sorted.length > 0) {
113117
compilerOptions[optionName] = sorted;
114118
}
@@ -120,7 +124,7 @@ export class ProjectAnalysisTelemetryCollector {
120124
compilerOptions,
121125
ecmaScriptVersions:
122126
this.ecmaScriptVersions.size > 0
123-
? Array.from(this.ecmaScriptVersions).sort()
127+
? Array.from(this.ecmaScriptVersions).sort((a, b) => a.localeCompare(b))
124128
: [NOT_DETECTED],
125129
programCreation: this.programCreation,
126130
};
@@ -134,10 +138,11 @@ export class ProjectAnalysisTelemetryCollector {
134138
if (values.length === 0) {
135139
return;
136140
}
137-
if (!this.compilerOptionValues.has(optionName)) {
138-
this.compilerOptionValues.set(optionName, new Set());
141+
let target = this.compilerOptionValues.get(optionName);
142+
if (!target) {
143+
target = new Set<string>();
144+
this.compilerOptionValues.set(optionName, target);
139145
}
140-
const target = this.compilerOptionValues.get(optionName)!;
141146
for (const value of values) {
142147
target.add(value);
143148
}
@@ -172,17 +177,18 @@ export class ProjectAnalysisTelemetryCollector {
172177
return [JSON.stringify(optionValue)];
173178
}
174179

175-
return [String(optionValue)];
180+
const json = JSON.stringify(optionValue);
181+
return json === undefined ? [] : [json];
176182
}
177183
}
178184

179185
function normalizeTypeScriptVersions(typeScriptSignals: string[]): string[] {
180186
const normalizedVersions = new Set<string>();
181187
for (const signal of typeScriptSignals) {
182188
try {
183-
const version = minVersion(signal)?.version;
184-
if (version) {
185-
normalizedVersions.add(version);
189+
const resolvedVersion = minVersion(signal)?.version;
190+
if (resolvedVersion) {
191+
normalizedVersions.add(resolvedVersion);
186192
}
187193
} catch {
188194
continue;
@@ -191,21 +197,27 @@ function normalizeTypeScriptVersions(typeScriptSignals: string[]): string[] {
191197
if (normalizedVersions.size === 0) {
192198
return [NOT_DETECTED];
193199
}
194-
return Array.from(normalizedVersions).sort();
200+
return Array.from(normalizedVersions).sort((a, b) => a.localeCompare(b));
195201
}
196202

197203
function getAvailablePackageJsons(): PackageJson[] {
198204
const packageJsons: PackageJson[] = [];
205+
let packageJsonFiles: Iterable<{ content: string | Buffer }>;
199206
try {
200-
for (const packageJsonFile of packageJsonStore.getPackageJsons().values()) {
201-
const content =
202-
typeof packageJsonFile.content === 'string'
203-
? packageJsonFile.content
204-
: packageJsonFile.content.toString();
207+
packageJsonFiles = packageJsonStore.getPackageJsons().values();
208+
} catch {
209+
return packageJsons;
210+
}
211+
for (const packageJsonFile of packageJsonFiles) {
212+
const content =
213+
typeof packageJsonFile.content === 'string'
214+
? packageJsonFile.content
215+
: packageJsonFile.content.toString();
216+
try {
205217
packageJsons.push(JSON.parse(stripBOM(content)) as PackageJson);
218+
} catch {
219+
continue;
206220
}
207-
} catch {
208-
return [];
209221
}
210222
return packageJsons;
211223
}

packages/jsts/src/program/factory.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ import { info } from '../../../shared/src/helpers/logging.js';
2525
import { getProgramCacheManager } from './cache/programCache.js';
2626
import { getCurrentFilesContext } from './cache/sourceFileCache.js';
2727
import { normalizeToAbsolutePath, type NormalizedAbsolutePath } from '../rules/helpers/files.js';
28-
29-
type ProgramCreationCallbacks = {
30-
onProgramCreated?: () => void;
31-
onProgramCreationError?: () => void;
32-
};
28+
import { getOptionalProjectAnalysisTelemetryCollector } from '../analysis/projectAnalysis/telemetry.js';
3329

3430
function createBuilderProgramWithHost(
3531
programOptions: ProgramOptions,
@@ -90,19 +86,16 @@ function createBuilderProgramAndHost(
9086
* enforces this at compile time.
9187
*
9288
* @param programOptions - Program options from createProgramOptions()
93-
* @param callbacks - Optional hooks triggered on creation success or failure
9489
* @returns Standard TypeScript Program
9590
*/
96-
export function createStandardProgram(
97-
programOptions: ProgramOptions,
98-
callbacks: ProgramCreationCallbacks = {},
99-
): ts.Program {
91+
export function createStandardProgram(programOptions: ProgramOptions): ts.Program {
92+
const telemetryCollector = getOptionalProjectAnalysisTelemetryCollector();
10093
try {
10194
const program = ts.createProgram(programOptions);
102-
callbacks.onProgramCreated?.();
95+
telemetryCollector?.recordProgramCreationSuccess();
10396
return program;
10497
} catch (error) {
105-
callbacks.onProgramCreationError?.();
98+
telemetryCollector?.recordProgramCreationFailure();
10699
throw error;
107100
}
108101
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
import { beforeEach, describe, it } from 'node:test';
18+
import { expect } from 'expect';
19+
import ts from 'typescript';
20+
import { ProjectAnalysisTelemetryCollector } from '../../src/analysis/projectAnalysis/telemetry.js';
21+
import { packageJsonStore } from '../../src/analysis/projectAnalysis/file-stores/index.js';
22+
23+
describe('project analysis telemetry', () => {
24+
beforeEach(() => {
25+
packageJsonStore.clearCache();
26+
});
27+
28+
it('should normalize and merge compiler options across programs', () => {
29+
const collector = new ProjectAnalysisTelemetryCollector();
30+
collector.recordCompilerOptions({
31+
lib: ['lib.es2022.d.ts', 'dom'],
32+
module: ts.ModuleKind.CommonJS,
33+
strict: true,
34+
paths: { '@/*': ['src/*'] },
35+
});
36+
collector.recordCompilerOptions({
37+
lib: ['lib.es2020.d.ts', 'dom'],
38+
module: ts.ModuleKind.NodeNext,
39+
strict: true,
40+
});
41+
42+
expect(collector.getTelemetry().compilerOptions).toEqual({
43+
lib: ['dom', 'es2020', 'es2022'],
44+
module: ['commonjs', 'nodenext'],
45+
paths: ['{"@/*":["src/*"]}'],
46+
strict: ['true'],
47+
});
48+
});
49+
50+
it('should ignore compiler option values that cannot be JSON stringified', () => {
51+
const collector = new ProjectAnalysisTelemetryCollector();
52+
collector.recordCompilerOptions({
53+
customOption: Symbol('custom'),
54+
} as unknown as ts.CompilerOptions);
55+
collector.recordCompilerOptions({ customOption: undefined } as unknown as ts.CompilerOptions);
56+
57+
expect(collector.getTelemetry().compilerOptions.customOption).toBeUndefined();
58+
});
59+
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ private static void addCompilerOptionTelemetry(
142142
String option,
143143
@Nullable List<String> values
144144
) {
145-
if (option == null || option.isBlank()) {
145+
if (option.isBlank()) {
146146
return;
147147
}
148148
addValueList(keyMapToSave, TELEMETRY_PREFIX + "typescript.compiler-options." + option, values);

0 commit comments

Comments
 (0)