Skip to content

Commit 77bd9b7

Browse files
authored
JS-1193 Escape backslashes in generated Java rule classes (#6629)
1 parent b2dae39 commit 77bd9b7

10 files changed

Lines changed: 53 additions & 65 deletions

File tree

docs/DEV.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,14 @@ export const fields = [
449449

450450
#### Field Properties
451451

452-
| Property | Required | Purpose |
453-
| --------------- | --------------------- | --------------------------------------------------------------------------- |
454-
| `field` | Yes | ESLint/schema key name |
455-
| `default` | Yes | Default value; also determines type (`number`, `string`, `boolean`, arrays) |
456-
| `description` | **For SQ visibility** | Makes the option visible in SonarQube UI |
457-
| `displayName` | No | SonarQube key if different from `field` |
458-
| `items` | For arrays | `{ type: 'string' }` or `{ type: 'integer' }` |
459-
| `customDefault` | No | Different default for SQ than JS/TS |
460-
| `fieldType` | No | Override SQ field type (e.g., `'TEXT'`) |
452+
| Property | Required | Purpose |
453+
| ------------- | --------------------- | --------------------------------------------------------------------------- |
454+
| `field` | Yes | ESLint/schema key name |
455+
| `default` | Yes | Default value; also determines type (`number`, `string`, `boolean`, arrays) |
456+
| `description` | **For SQ visibility** | Makes the option visible in SonarQube UI |
457+
| `displayName` | No | SonarQube key if different from `field` |
458+
| `items` | For arrays | `{ type: 'string' }` or `{ type: 'integer' }` |
459+
| `fieldType` | No | Override SQ field type (e.g., `'TEXT'`) |
461460

462461
### Making Options Visible in SonarQube
463462

packages/grpc/src/transformers/rule-configurations/jsts.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ type FieldDef = {
5353
field: string;
5454
displayName?: string;
5555
default: unknown;
56-
customDefault?: unknown;
5756
customForConfiguration?: (value: unknown) => unknown;
5857
};
5958

@@ -120,16 +119,6 @@ function parseParamValue(value: string, defaultValue: unknown) {
120119
return value;
121120
}
122121

123-
function getParseTarget(fieldDef: {
124-
default: unknown;
125-
customDefault?: unknown;
126-
customForConfiguration?: (value: unknown) => unknown;
127-
}) {
128-
return fieldDef.customForConfiguration && fieldDef.customDefault !== undefined
129-
? fieldDef.customDefault
130-
: fieldDef.default;
131-
}
132-
133122
/**
134123
* Build an object configuration from an array of field definitions.
135124
*
@@ -176,7 +165,7 @@ function buildObjectConfiguration(fieldDefs: FieldDef[], paramsLookup: Map<strin
176165
const paramValue = paramsLookup.get(sqKey);
177166

178167
if (paramValue !== undefined) {
179-
paramsObj[fieldDef.field] = parseParamValue(paramValue, getParseTarget(fieldDef));
168+
paramsObj[fieldDef.field] = parseParamValue(paramValue, fieldDef.default);
180169
}
181170
}
182171

@@ -230,7 +219,6 @@ function buildPrimitiveConfiguration(
230219
element: {
231220
default: unknown;
232221
displayName?: string;
233-
customDefault?: unknown;
234222
customForConfiguration?: (value: unknown) => unknown;
235223
},
236224
paramsLookup: Map<string, string>,
@@ -242,7 +230,7 @@ function buildPrimitiveConfiguration(
242230
if (sqKey) {
243231
const paramValue = paramsLookup.get(sqKey);
244232
if (paramValue !== undefined) {
245-
return parseParamValue(paramValue, getParseTarget(element));
233+
return parseParamValue(paramValue, element.default);
246234
}
247235
} else if (params.length > 0 && index === 0) {
248236
// Fallback for Type B primitives without displayName.

packages/grpc/tests/server.test.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,8 @@ describe('gRPC server', () => {
594594
expect(responseNoTrigger.issues?.length).toBe(0);
595595
});
596596

597-
it('should handle rule with customForConfiguration and customDefault (S1441 — quotes)', async () => {
598-
// S1441 has customDefault: true and customForConfiguration: `value ? "single" : "double"`
597+
it('should handle rule with customForConfiguration (S1441 — quotes)', async () => {
598+
// S1441 has default: 'single' and customForConfiguration handling SQ values 'true'/'false'.
599599
// When SQ sends singleQuotes='true', it should be transformed to 'single' for ESLint
600600
const content = 'const x = "hello";\n';
601601

@@ -633,9 +633,9 @@ describe('gRPC server', () => {
633633
expect(responseNoTrigger.issues?.length).toBe(0);
634634
});
635635

636-
it('should handle customDefault with number field in object config (S6418 — hard-coded secrets)', async () => {
637-
// S6418 config.ts has randomnessSensibility with default: 5 (number) and customDefault: '5.0' (string for Java).
638-
// The gRPC path should parse 'randomnessSensibility' as a number (from default: 5).
636+
it('should handle number field in object config (S6418 — hard-coded secrets)', async () => {
637+
// S6418 config.ts has randomnessSensibility with numeric default: 5.
638+
// The gRPC path should parse 'randomnessSensibility' as number.
639639
// Also tests that 'secretWords' string param is passed correctly.
640640
// Needs a high-entropy string to exceed the sensibility threshold.
641641
const content =
@@ -678,9 +678,8 @@ describe('gRPC server', () => {
678678
expect(responseNoTrigger.issues?.length).toBe(0);
679679
});
680680

681-
it('should handle customDefault with escaped regex string (S139 — trailing comments)', async () => {
682-
// S139 (line-comment-position) config.ts has ignorePattern with default: '^\\s*[^\\s]+$'
683-
// and customDefault: '^\\\\s*[^\\\\s]+$' (double-escaped for Java).
681+
it('should handle escaped regex string defaults (S139 — trailing comments)', async () => {
682+
// S139 (line-comment-position) config.ts has ignorePattern default: '^\\s*[^\\s]+$'.
684683
// The SQ key is 'pattern' (displayName).
685684

686685
// Multi-word trailing comment doesn't match default ignorePattern → should trigger
@@ -720,9 +719,30 @@ describe('gRPC server', () => {
720719
expect(responseNoTrigger.issues?.length).toBe(0);
721720
});
722721

723-
it('should handle customDefault with array of escaped regexes (S7718 — catch error name)', async () => {
724-
// S7718 config.ts has ignore field with default: array of regexes and
725-
// customDefault: same array with double-escaped regexes (for Java).
722+
it('should handle escaped regex defaults with optional $ prefix (S101 — class names)', async () => {
723+
// S101 default format is '^\\$?[A-Z][a-zA-Z0-9]*$' and should allow optional '$'.
724+
// This verifies the default from Java is correctly unescaped on the JS side.
725+
const content = 'interface $ZodCheckDef {}\ninterface my_interface {}\n';
726+
727+
const request: analyzer.IAnalyzeRequest = {
728+
analysisId: generateAnalysisId(),
729+
contextIds: {},
730+
sourceFiles: [{ relativePath: '/project/src/interface-name.ts', content }],
731+
activeRules: [
732+
{
733+
ruleKey: { repo: 'javascript', rule: 'S101' },
734+
params: [],
735+
},
736+
],
737+
};
738+
739+
const response = await client.analyze(request);
740+
expect(response.issues?.length).toBe(1);
741+
expect(response.issues?.[0].rule?.rule).toBe('S101');
742+
});
743+
744+
it('should handle escaped regex defaults in arrays (S7718 — catch error name)', async () => {
745+
// S7718 config.ts has ignore field default as an array of regexes.
726746
// The gRPC path should split comma-separated values into an array.
727747
const content = 'try { foo(); } catch (myVar) { throw myVar; }\n';
728748

packages/jsts/src/rules/S101/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export const fields = [
2424
field: 'format',
2525
description: 'Regular expression used to check the class names against.',
2626
default: String.raw`^\$?[A-Z][a-zA-Z0-9]*$`,
27-
customDefault: String.raw`^\\$?[A-Z][a-zA-Z0-9]*$`,
2827
},
2928
],
3029
] as const satisfies ESLintConfiguration;

packages/jsts/src/rules/S139/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export const fields = [
2424
field: 'ignorePattern',
2525
description: 'Pattern (JavaScript syntax) for text of trailing comments that are allowed.',
2626
default: `^\\s*[^\\s]+$`,
27-
customDefault: `^\\\\s*[^\\\\s]+$`,
2827
displayName: 'pattern',
2928
},
3029
],

packages/jsts/src/rules/S1441/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ export const fields = [
2222
{
2323
description: 'Set to true to require single quotes, false for double quotes.',
2424
default: 'single',
25-
customDefault: true,
2625
displayName: 'singleQuotes',
27-
customForConfiguration: (value: unknown) => (value ? 'single' : 'double'),
26+
customForConfiguration: (value: unknown) =>
27+
value === false || value === 'false' || value === 'double' ? 'double' : 'single',
2828
},
2929
[
3030
{

packages/jsts/src/rules/S6418/config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export const fields = [
2929
field: 'randomnessSensibility',
3030
description: 'Minimum shannon entropy threshold of the secret',
3131
default: 5,
32-
customDefault: '5.0',
33-
customForConfiguration: Number,
3432
},
3533
],
3634
] as const satisfies ESLintConfiguration;

packages/jsts/src/rules/S7718/config.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,6 @@ export const fields = [
3333
'^[cC][aA][uU][sS][eE]$',
3434
'^[rR][eE][aA][sS][oO][nN]$',
3535
],
36-
customDefault: [
37-
'^(e|ex)$',
38-
'[eE][xX][cC][eE][pP][tT][iI][oO][nN]$',
39-
'[eE][rR][rR]$',
40-
'^_',
41-
String.raw`^\\w\\$\\d+$`,
42-
'^[cC][aA][uU][sS][eE]$',
43-
'^[rR][eE][aA][sS][oO][nN]$',
44-
],
4536
},
4637
],
4738
] as const satisfies ESLintConfiguration;

packages/jsts/src/rules/helpers/configs.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,13 @@ type ESLintConfigurationDefaultProperty = {
2424
* Necessary for the property to show up in the SonarQube interface.
2525
* @param description will explain to the user what the property configures
2626
* @param displayName only necessary if the name of the property is different from the `field` name
27-
* @param customDefault only necessary if different default in SQ different than in JS/TS
2827
* @param items only necessary if type is 'array'
2928
* @param fieldType only necessary if you need to override the default fieldType in SQ
3029
* @param customForConfiguration replacement content how to pass this variable to the Configuration object
3130
*/
3231
export type ESLintConfigurationSQProperty = ESLintConfigurationDefaultProperty & {
3332
description: string;
3433
displayName?: string;
35-
customDefault?: Default;
3634
items?: {
3735
type: 'string' | 'integer';
3836
};
@@ -156,8 +154,8 @@ export function applyTransformations(
156154
// fieldDefinition is a single property: { default, customForConfiguration, ... }
157155
// mergedConfigEntry is a scalar value (string, number, boolean)
158156
//
159-
// Example — S1441 fieldDefinition = { default: 'single', customDefault: true, customForConfiguration: (v) => v ? 'single' : 'double' }
160-
// mergedConfigEntry = true (boolean from SQ)
157+
// Example — S1441 fieldDefinition = { default: 'single', customForConfiguration: (v) => ... }
158+
// mergedConfigEntry = 'true' (string from SQ)
161159
// After transform: 'single' (string expected by ESLint quotes rule)
162160
return fieldDefinition.customForConfiguration(mergedConfigEntry);
163161
}

tools/generate-java-rule-classes.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,8 @@ function generateBody(
150150
return;
151151
}
152152

153-
const getSQDefault = () => {
154-
return property.customDefault ?? property.default;
155-
};
156-
157153
const getJavaType = () => {
158-
const defaultValue = getSQDefault();
154+
const defaultValue = property.default;
159155
switch (typeof defaultValue) {
160156
case 'number':
161157
return 'int';
@@ -169,39 +165,39 @@ function generateBody(
169165
};
170166

171167
const getDefaultValueString = () => {
172-
const defaultValue = getSQDefault();
168+
const defaultValue = property.default;
173169
switch (typeof defaultValue) {
174170
case 'number':
175171
case 'boolean':
176172
return `"" + ${defaultValue}`;
177173
case 'string':
178-
return `"${defaultValue}"`;
174+
return `"${escapeJavaString(defaultValue)}"`;
179175
case 'object': {
180176
assert(Array.isArray(defaultValue));
181-
return `"${defaultValue.join(',')}"`;
177+
return `"${escapeJavaString(defaultValue.join(','))}"`;
182178
}
183179
}
184180
};
185181

186182
const getDefaultValue = () => {
187-
const defaultValue = getSQDefault();
183+
const defaultValue = property.default;
188184
switch (typeof defaultValue) {
189185
case 'number':
190186
case 'boolean':
191187
return `${defaultValue.toString()}`;
192188
case 'string':
193-
return `"${defaultValue}"`;
189+
return `"${escapeJavaString(defaultValue)}"`;
194190
case 'object':
195191
assert(Array.isArray(defaultValue));
196-
return `"${defaultValue.join(',')}"`;
192+
return `"${escapeJavaString(defaultValue.join(','))}"`;
197193
}
198194
};
199195

200196
const defaultFieldName = 'field' in property ? (property.field as string) : 'value';
201197
const defaultValue = getDefaultValueString();
202198
imports.add('import org.sonar.check.RuleProperty;');
203199
result.push(
204-
`@RuleProperty(key="${property.displayName ?? defaultFieldName}", description = "${property.description}", defaultValue = ${defaultValue}, type="${property.fieldType || ''}")`,
200+
`@RuleProperty(key="${escapeJavaString(property.displayName ?? defaultFieldName)}", description = "${escapeJavaString(property.description)}", defaultValue = ${defaultValue}, type="${escapeJavaString(property.fieldType || '')}")`,
205201
);
206202
result.push(`${getJavaType()} ${defaultFieldName} = ${getDefaultValue()};`);
207203
hasSQProperties = true;

0 commit comments

Comments
 (0)