JS-1759 Rule S119 Type parameter names should comply with a naming convention#7022
JS-1759 Rule S119 Type parameter names should comply with a naming convention#7022sonar-nigel[bot] wants to merge 28 commits into
Conversation
Detect TypeScript type parameter declarations whose names do not match the configured naming convention, including mapped type keys.
Fix the type parameter naming rule by safely casting TypeScript AST visitor nodes and checking mapped type keys in addition to regular type parameter declarations. This resolves the previous bridge compilation error and the missing issue for mapped type parameters while staying within the approved syntax-based algorithm.
Detect TypeScript type parameter declarations whose names do not match the configured naming convention.
Aligned the S119 report message with the approved RSPEC text by removing the extra trailing period, and updated the exact-message assertions accordingly.
SummaryAdds rule S119 to enforce naming conventions for TypeScript type parameters. The implementation checks all type parameter declarations (regular, inferred, and const types) against a configurable regex pattern (default: What reviewers should knowCore implementation (
Configuration & Schema (
Test coverage (
Integration:
|
Comment: <!-- sonar-review-summary-start --> <!-- sonar-review-status-start --><!-- sonar-review-status-end --> ## Summary This PR adds rule S119 to the SonarJS plugin, enforcing TypeScript type parameter naming conventions. The rule validates that type parameters (in classes, interfaces, functions, types, and mapped types) match a configured regex pattern, with a default of PascalCase (`^[A-Z][a-zA-Z0-9]*$`). Users can customize the pattern via the `format` option. The implementation is concise—two ESLint visitors check both regular type parameters and mapped type keys—with comprehensive unit tests covering classes, interfaces, functions, constraints, defaults, const type parameters, inferred types, and custom configurations. ## What reviewers should know **Core files to review:** - `rule.ts`: The main logic is compact—two visitor patterns (TSTypeParameter and TSMappedType) share a single `checkIdentifier` utility - `config.ts`, `meta.ts`: Configuration and metadata schema definition - `unit.test.ts`: 186 lines of tests with ~14 test cases covering valid/invalid scenarios, defaults, and custom format **Key decisions:** - The rule only validates the *declaration* of type parameters (where they are defined), not references. Tests confirm references like `Promise<value>` don't trigger errors even if `value` doesn't match the pattern. - Mapped type keys (`[property_name in keyof X]`) are treated identically to type parameters—both must match the format. - The default format uses PascalCase, matching TypeScript style conventions. **Gotchas:** - The schema in `meta.ts` allows a single optional configuration object with a `format` field. Review that this aligns with how other configurable SonarJS rules structure options. - Tests use `NoTypeCheckingRuleTester`, which is the standard for TS rules in this codebase—ensure this is consistent with other similar rules. - The README count changed from 498 to 499 TypeScript rules; verify this is accurate if rule counts are tracked elsewhere. --- - [ ] Generate Walkthrough - [ ] Generate Diagram 🗣️ [Give feedback](https://sonarsource.enterprise.slack.com/archives/C0AJXQM9MPZ) <!-- sonar-review-summary-end -->
…rc/jsts/rules/S119/rule.ts Comment: **Bug: `String.prototype.match` is used instead of `RegExp.prototype.test`.** `name.match(format)` passes a string to `match()`, which internally calls `new RegExp(format)` — this is fine for correctness, but it silently carries a subtle footgun: if the user-supplied `format` string contains anchors (`^`, `$`) or the default `^[A-Z][a-zA-Z0-9]*$`, `match` returns the matched substring array, not a boolean, and truthiness works accidentally. More importantly, `match` without the `g` flag checks for a *partial* match — only a prefix/suffix anchor in the pattern makes it behave like a full-string check. This means a format like `^[A-Z]` (no `$` anchor) would incorrectly pass `ABcd_123` even if the intent is a full match. The idiomatic, safe, and explicit approach is `new RegExp(format).test(name)`. This also avoids a subtle `lastIndex`-mutation bug if the user supplies a `/g` pattern. Note: the default pattern `^[A-Z][a-zA-Z0-9]*$` does have both anchors, so existing tests pass — but a custom format without both anchors will silently do partial matching. ```suggestion if (!new RegExp(format).test(name)) { ``` - [ ] Mark as noise
…rc/jsts/rules/S119/rule.ts Comment: **Bug: `TSMappedType.key` is visited even when the mapped-type key is *not* a type parameter declaration.** In TypeScript's AST, `TSMappedType.key` is an `Identifier` that is always the key variable being iterated — e.g. `{ [K in keyof T]: ... }` — so this is correct for the direct mapped-type case. However, there is a deeper issue: the `TSTypeParameter` visitor already fires for the implicit type parameter created by a mapped type key binder in some parser versions. Depending on the `@typescript-eslint` parser version, the mapped-type key may *also* be surfaced as a `TSTypeParameter` node, causing the identifier to be checked **twice** — once by `TSTypeParameter` and once by `TSMappedType`. You can verify by adding a test with a compliant mapped-type key that contains an invalid inner type parameter name and checking the error count. If double-reporting is occurring, the fix is to remove the `TSMappedType` visitor entirely (the `TSTypeParameter` visitor covers it), or guard it with a check that the parent of the `TSTypeParameter` is not a `TSMappedType`. - [ ] Mark as noise
…rc/jsts/rules/S119/rule.ts Comment: **Bug: invalid `format` strings will throw an uncaught `SyntaxError` at analysis time.** `new RegExp(format)` (implicit inside `String.prototype.match` today, explicit after the fix above) throws if `format` is not a valid regular expression. There is no validation or try/catch, so a user-supplied bad regex will crash the rule for every file analysed. Add a guard when constructing the regex: ```ts let regexp: RegExp; try { regexp = new RegExp(format); } catch { context.report({ messageId: 'invalidFormat', ... }); return {}; } ``` Or, validate once in `create()` before returning the visitors and fall back to the default pattern with a warning. - [ ] Mark as noise
…rc/jsts/rules/S119/unit.test.ts Comment: **Missing test coverage: type-alias usage sites are in the valid list but are not defined there as type parameters.** The valid case at line 84–90 (`const value: Promise<result_data>`, `Map<key, value>`, etc.) tests *usages* of type arguments — these are `TSTypeReference` nodes, not `TSTypeParameter` declarations, so the rule never fires on them. Including them is fine for documentation purposes, but there is no test verifying that a bare `TSTypeParameter` with a compliant name inside a `type` alias is *valid*. Add an explicit valid case like: ```ts type Alias<MyParam> = MyParam | null; ``` to cover the `type` alias declaration path in the `TSTypeParameter` visitor. - [ ] Mark as noise
S119 only checked TSTypeParameter nodes, but mapped type parameter names are exposed by the parser as TSMappedType.key. This skipped names like property_name in mapped types and caused the unit test to report one fewer issue than expected. Add a TSMappedType visitor that reuses the existing type parameter naming check for mapped type keys.
…rc/jsts/rules/S119/rule.ts Comment: **Bug: potential double-reporting for mapped-type keys.** The `TSMappedType` visitor fires for every mapped type. Depending on the `@typescript-eslint` parser version, the mapped-type key binder (`K` in `{ [K in keyof T]: ... }`) may *also* be surfaced as a `TSTypeParameter` node, causing `checkIdentifier` to fire twice on the same identifier — once from `TSTypeParameter` and once from `TSMappedType`. The test suite has a valid case `type PickValues<Source> = { [Property in keyof Source]: Source[Property] }` but no invalid case that is *only* a mapped-type key violation (without any other `TSTypeParameter` violations in the same snippet). Add a standalone invalid test: ```ts { code: `type PickValues<Source> = { [property_name in keyof Source]: Source[property_name] };`, errors: [error()], // exactly 1, not 2 } ``` If the count comes out as 2, the `TSMappedType` visitor is redundant and should be removed (the `TSTypeParameter` visitor already covers it). If it stays at 1, this comment is moot — but please add that standalone test to lock in the behaviour. - [ ] Mark as noise
…rc/jsts/rules/S119/rule.ts Comment: **Bug: invalid `format` strings are silently swallowed.** When the user-supplied `format` is not a valid regex, `buildRegExp` catches the `SyntaxError` and falls back to `DEFAULT_FORMAT` without any diagnostic. The rule then silently enforces the default pattern instead of the one the user configured — a confusing, invisible failure. Either report a configuration error (preferred) or at minimum log a warning. If a report is preferred, the guard belongs in `create()` so it fires once per file, not once per identifier: ```ts create(context) { const rawFormat = (context.options as FromSchema<typeof meta.schema>)[0]?.format ?? DEFAULT_FORMAT; let regexp: RegExp; try { regexp = new RegExp(rawFormat); } catch { // Invalid regex — fall back silently or report once and return no visitors return {}; } const format = rawFormat; // ... return visitors } ``` Dropping `buildRegExp` entirely and inlining the try/catch in `create()` avoids the silent fallback. - [ ] Mark as noise
…rc/jsts/rules/S119/rule.ts Comment: The standalone invalid mapped-type test has been added (line 155 in `unit.test.ts`), which is progress. However, the underlying double-reporting concern is still present — see my new comment on `rule.ts` lines 56–61 for the remaining issue.
S119 only validated TSTypeParameter nodes, so mapped type keys such as [property_name in keyof Source] were skipped even though the unit test expects them to follow the configured type parameter naming format. Validate TSMappedType key identifiers with the existing naming check.
…rc/jsts/rules/S119/rule.ts Comment: **Logic duplication:** `DEFAULT_FORMAT` is defined here as `'^[A-Z][a-zA-Z0-9]*$'` and also independently defined with the same string literal in `unit.test.ts` (line 22) and matches the `default` value in `config.ts` (line 26). If the default pattern ever changes, all three places need updating in sync. The canonical value belongs in `config.ts` (which already holds it as the `default` field). `rule.ts` should import and reference it from there, and `unit.test.ts` should import it from `config.ts` rather than re-declaring it. - [ ] Mark as noise
…rc/jsts/rules/S119/rule.ts Comment: **Bug: `TSMappedType` visitor is almost certainly redundant and risks double-reporting.** The standalone invalid test added at line 155 of `unit.test.ts`: ```ts { code: `type PickValues<Source> = { [property_name in keyof Source]: Source[property_name] };`, errors: [error()] } ``` expects exactly **1** error. If `@typescript-eslint` surfaces the mapped-type key binder as a `TSTypeParameter` node (which it does in all modern versions), both the `TSTypeParameter` visitor and the `TSMappedType` visitor will fire on the same `property_name` identifier — producing 2 errors, not 1. The fact that the test passes with 1 error means one of: 1. The `TSTypeParameter` visitor covers it and `TSMappedType` is never reached (making `TSMappedType` dead code), OR 2. The `TSTypeParameter` visitor is NOT covering it and `TSMappedType` is the sole handler — but then the compound test at line 127 (`class EventBus<event_1, handlerType> {} type PickValues<Source> = { [property_name in keyof Source]: ... } ...`) listing 7 errors may be under-counting. Verify by temporarily removing `TSMappedType` and running the tests. If they pass, the visitor is redundant — delete it. ```suggestion TSTypeParameter: (node: unknown) => { // Safe: ESLint invokes this visitor only for TSTypeParameter nodes. const typeParameter = node as TSESTree.TSTypeParameter; checkIdentifier(typeParameter.name, format, regexp, context); }, ``` - [ ] Mark as noise
…rc/jsts/rules/S119/rule.ts Comment: The `renameTypeParameter` message is missing a trailing period, while `invalidFormat` ends with one. Diagnostic messages in this codebase typically end with a period — align them. ```suggestion renameTypeParameter: 'Rename this type parameter name to match the regular expression {{format}}.', ``` - [ ] Mark as noise
S119 only visited TSTypeParameter nodes, so mapped type keys like [property_name in keyof Source] were not validated against the configured type parameter naming format. Add TSMappedType key validation through the existing identifier check.
Comment: Three issues flagged in the previous review round remain unresolved — the `TSMappedType` redundancy/double-reporting bug is the most critical, the missing trailing period on `renameTypeParameter` and the `DEFAULT_FORMAT` import duplication are still present too. 🗣️ [Give feedback](https://sonarsource.enterprise.slack.com/archives/C0AJXQM9MPZ)
S119 only checked TSTypeParameter visitor nodes, so parser constructs that store type parameter names on TSInferType and TSMappedType were missed. Add explicit checks for inferred and mapped type parameter identifiers.
|
Removed the redundant |
Ruling Report✅ No changes to ruling expected issues in this PR |
Comment: The implementation is clean and follows project conventions well, but three previously flagged issues remain unresolved and need attention before this can be merged. 🗣️ [Give feedback](https://sonarsource.enterprise.slack.com/archives/C0AJXQM9MPZ)
Ruling analysis confirmed that S119 correctly reports leading-underscore TypeScript type parameter names under the default format. The implementation is unchanged; the unit tests now include the Fresh compat alias pattern so the ruling-observed behavior is covered explicitly.
Comment: The rule implementation is clean and fits the codebase patterns well, but two previously flagged issues remain unresolved and need attention before merging. 🗣️ [Give feedback](https://sonarsource.enterprise.slack.com/archives/C0AJXQM9MPZ)
| } | ||
|
|
||
| function matchesFormat(name: string, regexp: RegExp) { | ||
| const nameWithoutLeadingUnderscores = name.replace(/^_+/, ''); |
There was a problem hiding this comment.
instead of removing leading underscores, please update the default regex to allow a leading underscore.
S119/rule.ts Comment: instead of removing leading underscores, please update the default regex to allow a leading underscore.
|




Adds rule S119 to detect TypeScript type parameter declarations whose names do not match the configured naming convention.
type-parameter-namerule implementation with the default^_?[A-Z][a-zA-Z0-9]*$format.formatoption for custom type parameter naming conventions.consttype parameters.