Skip to content

Commit 26cd7fd

Browse files
vdiezzgliczclaude
committed
JS-937 Support grpc in Node process (#6049)
Co-authored-by: zglicz <michal.zgliczynski@sonarsource.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 46204c0 commit 26cd7fd

32 files changed

Lines changed: 2581 additions & 462 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ coverage/
99
test-report.xml
1010

1111
packages/jsts/src/rules/*/generated-meta.ts
12+
packages/grpc/src/proto/language_analyzer.js
13+
packages/grpc/src/proto/language_analyzer.d.ts
1214
sonarjs-1.0.0.tgz
1315

1416
# IntelliJ IDEA

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ sonar-plugin/sonar-javascript-plugin/target
2929
sonar-plugin/sonar-javascript-plugin/src/test/resources
3030

3131
tools/fetch-node/downloads
32+
packages/grpc/src/proto/es6-wrapper.js

Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ WORKDIR /app
66
# Copy the bundled output from esbuild
77
COPY ./bin/ ./bin/
88
COPY package.json ./
9-
COPY packages/bridge/src/openrpc-server.json ./
109

1110
# Expose default port (adjust if needed)
1211
EXPOSE 3000
1312

1413
# Run the bundled server
15-
CMD ["node", "./bin/server.cjs", "3000", "0.0.0.0"]
14+
CMD ["node", "./bin/grpc-server.cjs", "3000"]
1615

docs/DEV.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,305 @@ You can simply copy and paste compliant and non-compliant examples from your RSP
307307
- Use comment-based tests with `package.json` dependencies dependent rule: [PR](<[TBD](https://github.com/SonarSource/SonarJS/pull/4443/files#diff-92d7c68b7e4cc945d0f128acbd458648eb8021903587c1ee7025243f2fae89d2)>)
308308
- Use ESLint's Rule tester with `package.json` dependencies dependent rule: [PR](<[TBD](https://github.com/SonarSource/SonarJS/commit/dc9435738093286869edff742c90d17d74e39b1c#diff-55f5136cfbed4170ed04f718f78f46015d6bb1f78c26403e036136211a333425R154-R213)>)
309309

310+
## Rule Options Architecture
311+
312+
This section explains how rule options (configurations) work across the SonarJS stack.
313+
314+
### Overview
315+
316+
There are two parallel workflows for requesting JS/TS analysis from Node.js:
317+
318+
**1. SonarQube workflow (HTTP bridge via WebSocket):**
319+
320+
```
321+
SonarQube UI → Java Check Class → HTTP/WebSocket → analyzeProject() → ESLint Linter
322+
323+
configurations() returns
324+
typed objects (int, boolean, etc.)
325+
```
326+
327+
**2. External workflow (gRPC - without SonarQube):**
328+
329+
```
330+
External Client → gRPC → transformers.ts → analyzeProject() → ESLint Linter
331+
332+
parseParamValue() converts
333+
string params to typed values
334+
```
335+
336+
The key difference is that SonarQube's Java side sends already-typed values via `configurations()`, while the gRPC endpoint receives string key-value pairs that need type parsing.
337+
338+
Each rule can have configurable options defined in several places that serve different purposes.
339+
340+
### File Structure for a Rule
341+
342+
Each rule lives in `packages/jsts/src/rules/SXXXX/` with these key files:
343+
344+
| File | Purpose |
345+
| ------------------- | ---------------------------------------------------------------------------- |
346+
| `rule.ts` | Rule implementation (ESLint rule factory) |
347+
| `meta.ts` | Manual metadata: `implementation`, `eslintId`, `schema`, re-exports `fields` |
348+
| `config.ts` | Option definitions with `fields` array |
349+
| `generated-meta.ts` | Auto-generated: `defaultOptions`, `sonarKey`, `scope`, `languages` |
350+
351+
### Implementation Types
352+
353+
The `implementation` field in `meta.ts` determines how a rule is structured:
354+
355+
#### `original`
356+
357+
Rules written from scratch for SonarJS. If the rule accepts options, it defines its own JSON Schema in `meta.ts`. Rules without options don't need a schema or config.ts:
358+
359+
```typescript
360+
// S100/meta.ts
361+
export const implementation = 'original';
362+
export const eslintId = 'function-name';
363+
export * from './config.js';
364+
import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema';
365+
export const schema = {
366+
type: 'array',
367+
items: [{ type: 'object', properties: { format: { type: 'string' } } }],
368+
} as const satisfies JSONSchema4;
369+
```
370+
371+
#### `decorated`
372+
373+
Rules that wrap/extend an existing ESLint rule, adding SonarJS-specific behavior. They may optionally define a `schema` if needed:
374+
375+
```typescript
376+
// S109/meta.ts - no schema, uses external rule's schema at runtime
377+
export const implementation = 'decorated';
378+
export const eslintId = 'no-magic-numbers';
379+
export const externalRules = [
380+
{ externalPlugin: 'typescript-eslint', externalRule: 'no-magic-numbers' },
381+
];
382+
export * from './config.js';
383+
```
384+
385+
```typescript
386+
// S107/meta.ts - explicit schema (when customization is needed)
387+
export const implementation = 'decorated';
388+
export const eslintId = 'max-params';
389+
export const externalRules = [{ externalPlugin: 'eslint', externalRule: 'max-params' }];
390+
export * from './config.js';
391+
export const schema = {
392+
/* ... */
393+
} as const satisfies JSONSchema4;
394+
```
395+
396+
#### `external`
397+
398+
Rules that directly use an ESLint rule without modification. The schema is inherited from the external rule at runtime. Some external rules expose user-configurable options via `config.ts` (e.g., S103, S139, S1441):
399+
400+
```typescript
401+
// S106/meta.ts
402+
export const implementation = 'external';
403+
export const eslintId = 'no-console';
404+
export const externalPlugin = 'eslint';
405+
export * from './config.js';
406+
```
407+
408+
### The `fields` Array (`config.ts`)
409+
410+
The `fields` array is the **source of truth** for rule options. It defines:
411+
412+
- ESLint field names
413+
- Default values (which determine types)
414+
- SonarQube UI descriptions
415+
- Key mappings when SQ and ESLint names differ
416+
417+
```typescript
418+
// S107/config.ts
419+
export const fields = [
420+
[
421+
{
422+
field: 'max', // ESLint option name
423+
displayName: 'maximumFunctionParameters', // SonarQube UI name (optional)
424+
description: 'Maximum authorized...', // Shows in SQ UI
425+
default: 7, // Default value & type inference
426+
},
427+
],
428+
] as const satisfies ESLintConfiguration;
429+
```
430+
431+
#### Field Properties
432+
433+
| Property | Required | Purpose |
434+
| --------------- | --------------------- | --------------------------------------------------------------------------- |
435+
| `field` | Yes | ESLint/schema key name |
436+
| `default` | Yes | Default value; also determines type (`number`, `string`, `boolean`, arrays) |
437+
| `description` | **For SQ visibility** | Makes the option visible in SonarQube UI |
438+
| `displayName` | No | SonarQube key if different from `field` |
439+
| `items` | For arrays | `{ type: 'string' }` or `{ type: 'integer' }` |
440+
| `customDefault` | No | Different default for SQ than JS/TS |
441+
| `fieldType` | No | Override SQ field type (e.g., `'TEXT'`) |
442+
443+
### Making Options Visible in SonarQube
444+
445+
**A field is only visible in SonarQube if it has a `description`.**
446+
447+
The Java code generator (`tools/generate-java-rule-classes.ts`) checks:
448+
449+
```typescript
450+
function isSonarSQProperty(property): property is ESLintConfigurationSQProperty {
451+
return property.description !== undefined;
452+
}
453+
```
454+
455+
Fields without `description` are internal-only defaults that users cannot configure.
456+
457+
**Example - S109 (Magic Numbers):**
458+
459+
```typescript
460+
// S109/config.ts - NO descriptions, so not exposed in SQ
461+
export const fields = [
462+
[
463+
{ field: 'ignore', default: [0, 1, -1, 24, 60] }, // Internal only
464+
{ field: 'ignoreDefaultValues', default: true }, // Internal only
465+
],
466+
] as const satisfies ESLintConfiguration;
467+
```
468+
469+
**Example - S2068 (Hardcoded Credentials):**
470+
471+
```typescript
472+
// S2068/config.ts - HAS description, so visible in SQ
473+
export const fields = [
474+
[
475+
{
476+
field: 'passwordWords',
477+
items: { type: 'string' },
478+
description: 'Comma separated list of words identifying potential passwords.',
479+
default: ['password', 'pwd', 'passwd', 'passphrase'],
480+
},
481+
],
482+
] as const satisfies ESLintConfiguration;
483+
```
484+
485+
### Key Mapping: SonarQube ↔ ESLint
486+
487+
When SonarQube and ESLint use different names for the same option:
488+
489+
| SonarQube Key | ESLint Key | Mapping |
490+
| --------------------------- | ---------- | ------------------------------------------------------- |
491+
| `maximumFunctionParameters` | `max` | `displayName: 'maximumFunctionParameters'` in config.ts |
492+
| `format` | `format` | No `displayName` needed (same name) |
493+
494+
The transformation layer (`packages/grpc/src/transformers.ts`) handles this mapping at runtime.
495+
496+
### JSON Schema vs `fields`
497+
498+
| Aspect | JSON Schema (`meta.ts`) | `fields` (`config.ts`) |
499+
| ---------------- | ----------------------------- | ------------------------------------- |
500+
| **Purpose** | ESLint validation | SQ UI + defaults + key mapping |
501+
| **Used by** | ESLint at runtime | Java codegen, meta generation, linter |
502+
| **Required for** | `original` rules with options | All rules with options |
503+
| **Defines** | Structure & constraints | Defaults, descriptions, SQ keys |
504+
505+
**Important:** The schema is for ESLint validation. The `fields` array provides default values and metadata for SonarQube integration. For `original` rules with options, both schema and fields must be kept in sync manually. For `decorated`/`external` rules, the schema is inherited from the external rule at runtime.
506+
507+
### `defaultOptions` in `generated-meta.ts`
508+
509+
The `npm run generate-meta` script reads `fields` and generates `defaultOptions`:
510+
511+
```typescript
512+
// generated-meta.ts (auto-generated)
513+
export const meta = {
514+
// ...
515+
defaultOptions: [
516+
{ format: '^[_a-z][a-zA-Z0-9]*$' }, // From fields[0][0].default
517+
],
518+
};
519+
```
520+
521+
This is extracted using the `defaultOptions()` helper from `helpers/configs.ts`.
522+
523+
### How Options Flow at Runtime
524+
525+
#### SonarQube workflow (HTTP/WebSocket)
526+
527+
1. **Java Side**: `@RuleProperty` fields are read, `configurations()` returns typed `List<Object>` (e.g., `Map.of("max", 7)`)
528+
2. **Transport**: Gson serializes to JSON with proper types preserved
529+
3. **Linter**: `linter.ts:createRulesRecord()` merges defaults with user config:
530+
```typescript
531+
rules[`sonarjs/${rule.key}`] = [
532+
'error',
533+
...merge(defaultOptions(ruleMeta.fields), rule.configurations),
534+
];
535+
```
536+
537+
#### gRPC workflow (external clients)
538+
539+
1. **Client**: Sends rule params as string key-value pairs via proto3
540+
2. **Transformer**: `transformers.ts` maps SQ keys → ESLint keys and parses string values to proper types
541+
3. **Linter**: Same merging as above
542+
543+
### Type Parsing from Strings (gRPC only)
544+
545+
The gRPC workflow receives all param values as strings. The transformer parses them based on the `default` value type in `fields`:
546+
547+
| Default Type | Input String | Parsed Result |
548+
| ------------ | ------------ | ----------------- |
549+
| `number` | `"5"` | `5` |
550+
| `boolean` | `"true"` | `true` |
551+
| `string` | `"pattern"` | `"pattern"` |
552+
| `string[]` | `"a,b,c"` | `["a", "b", "c"]` |
553+
| `number[]` | `"1,2,3"` | `[1, 2, 3]` |
554+
555+
### Adding Options to an Existing Rule
556+
557+
1. **Update `config.ts`** with the new field in the `fields` array
558+
2. **Add `description`** if it should be visible in SonarQube
559+
3. **Update `meta.ts` schema** (for `original`/`decorated` rules) to match
560+
4. **Run `npm run generate-meta`** to update `generated-meta.ts`
561+
5. **Run `npm run generate-java-rule-classes`** to update Java check classes
562+
563+
### Common Patterns
564+
565+
#### Object-style configuration (most common):
566+
567+
```typescript
568+
// config.ts
569+
export const fields = [
570+
[
571+
{ field: 'max', description: '...', default: 7 },
572+
{ field: 'ignoreIIFE', description: '...', default: false },
573+
],
574+
] as const satisfies ESLintConfiguration;
575+
576+
// ESLint receives: [{ max: 7, ignoreIIFE: false }]
577+
```
578+
579+
#### Primitive configuration:
580+
581+
```typescript
582+
// config.ts
583+
export const fields = [
584+
{ default: '^[a-z]+$' }, // Single non-array element
585+
] as const satisfies ESLintConfiguration;
586+
587+
// ESLint receives: ['^[a-z]+$']
588+
```
589+
590+
#### Array options (comma-separated in SQ):
591+
592+
```typescript
593+
// config.ts
594+
export const fields = [
595+
[
596+
{
597+
field: 'passwordWords',
598+
items: { type: 'string' }, // Required for Java codegen
599+
description: 'Comma separated list...',
600+
default: ['password', 'pwd'],
601+
},
602+
],
603+
] as const satisfies ESLintConfiguration;
604+
605+
// SQ sends: "password,pwd,secret"
606+
// ESLint receives: [{ passwordWords: ['password', 'pwd', 'secret'] }]
607+
```
608+
310609
## Misc
311610

312611
- Use issue number for a branch name, e.g. `issue-1234`

0 commit comments

Comments
 (0)