Skip to content

Commit 9798230

Browse files
francois-mora-sonarsourceclaudegithub-actions[bot]
authored
JS-1450 chore: update Claude skills with guidance from PR review feedback (#6584)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 1ec30b1 commit 9798230

File tree

3 files changed

+311
-8
lines changed

3 files changed

+311
-8
lines changed

.claude/skills/helpers/SKILL.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,23 @@ Helper functions are located in `packages/jsts/src/rules/helpers/`. These provid
2424

2525
## Usage Guidance
2626

27-
When implementing a fix:
28-
1. Review the `packages/jsts/src/rules/helpers/` directory for existing utilities
29-
2. Use existing helpers instead of writing custom logic where possible
30-
3. This ensures consistency and reduces code duplication
27+
**Before writing any new function**, search `packages/jsts/src/rules/helpers/` for an existing equivalent. Key files to check:
28+
- `ast.ts` — generic AST utilities (child enumeration, node classification, ancestor walking)
29+
- `vue.ts` — Vue-specific helpers
30+
- `react.ts` — React/JSX-specific helpers
31+
32+
Reviewers will reject re-implementations of existing helpers.
33+
34+
**Anti-pattern:** Iterating `Object.keys(node)` to enumerate child nodes. Use the existing `childrenOf` helper instead:
35+
```typescript
36+
// ❌ Do not do this
37+
for (const key of Object.keys(node)) { ... }
38+
39+
// ✅ Use this
40+
for (const child of childrenOf(node)) { ... }
41+
```
42+
43+
If you write a **new** utility function that only operates on `estree`/`TSESTree` nodes with no rule-specific domain logic, place it in `helpers/ast.ts` (or `helpers/vue.ts`, `helpers/react.ts` for domain-specific utilities) — not inside the individual rule file. Functions buried in a rule file are invisible to future implementers.
3144

3245
## Finding Helpers
3346

@@ -37,3 +50,21 @@ ls packages/jsts/src/rules/helpers/
3750
```
3851

3952
Read helper implementations to understand their usage and parameters.
53+
54+
## Helper Contracts
55+
56+
### childrenOf
57+
58+
`childrenOf(node)` always returns `estree.Node[]`. Do not write custom type guard predicates or `Array.isArray()` checks on its return values — they are dead code:
59+
60+
```typescript
61+
// ❌ Redundant — childrenOf already guarantees estree.Node[]
62+
for (const child of childrenOf(node)) {
63+
if (typeof child === 'object' && 'type' in child) { ... }
64+
}
65+
66+
// ✅ Iterate directly
67+
for (const child of childrenOf(node)) {
68+
// child is already estree.Node
69+
}
70+
```

.claude/skills/rule-implementation/SKILL.md

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,275 @@ After modifying configuration or metadata files, regenerate the derived files:
131131

132132
- `npm run generate-meta` - Regenerate rule metadata after changes to `meta.ts` or `config.ts`
133133
- `npm run generate-java-rule-classes` - Regenerate Java rule classes (required after `config.ts` changes)
134+
135+
---
136+
137+
## AST Traversal Strategy
138+
139+
### Prefer ESLint selector-based node collection over recursive walks
140+
141+
When implementing a rule, use ESLint's selector-based dispatch instead of walking the AST manually:
142+
143+
```typescript
144+
// ❌ Recursive walk — expensive and brittle
145+
create(context) {
146+
return {
147+
FunctionDeclaration(node) {
148+
const returns = collectReturns(node.body); // recursive walk inside
149+
}
150+
};
151+
}
152+
153+
// ✅ Selector + side-table — ESLint dispatches once per node
154+
create(context) {
155+
const returnsByFunction = new Map();
156+
return {
157+
ReturnStatement(node) {
158+
const fn = getEnclosingFunction(node);
159+
if (fn) returnsByFunction.set(fn, [...(returnsByFunction.get(fn) ?? []), node]);
160+
},
161+
'FunctionDeclaration:exit'(node) {
162+
const returns = returnsByFunction.get(node) ?? [];
163+
// analyse returns
164+
}
165+
};
166+
}
167+
```
168+
169+
Selector-based approaches let ESLint dispatch to your handler exactly once per matching node at no extra traversal cost, and automatically handle all node types including future additions to the language.
170+
171+
### When recursive traversal is unavoidable, use visitorKeys — not switch
172+
173+
If you must traverse a subtree recursively inside a helper (e.g. `containsAssignment`, `nodeHasReturn`), enumerate child properties generically using ESLint's `visitorKeys` instead of a manual `switch`:
174+
175+
```typescript
176+
// ❌ Manual switch — silently drops unlisted node types
177+
function containsReturn(node: estree.Node): boolean {
178+
switch (node.type) {
179+
case 'ReturnStatement': return true;
180+
case 'BlockStatement': return node.body.some(containsReturn);
181+
default: return false; // silently misses loops, try/catch, switch bodies...
182+
}
183+
}
184+
185+
// ✅ Generic traversal via visitorKeys
186+
import { visitorKeys } from '@typescript-eslint/visitor-keys';
187+
function containsReturn(node: estree.Node): boolean {
188+
if (node.type === 'ReturnStatement') return true;
189+
const keys = visitorKeys[node.type] ?? [];
190+
return keys.some(key => {
191+
const child = (node as Record<string, unknown>)[key];
192+
if (Array.isArray(child)) return child.some(c => c && containsReturn(c as estree.Node));
193+
if (child && typeof child === 'object') return containsReturn(child as estree.Node);
194+
return false;
195+
});
196+
}
197+
```
198+
199+
---
200+
201+
## TypeScript Type Safety
202+
203+
### Unsafe type assertions must be guarded or explained
204+
205+
Do not cast AST nodes silently. A cast is safe only if it holds for every node type the upstream rule can report on.
206+
207+
```typescript
208+
// ❌ Silent cast — crashes if upstream rule ever reports a non-Identifier node
209+
const identifier = tsNode as TSESTree.Identifier;
210+
211+
// ✅ Type guard
212+
if (tsNode.type !== 'Identifier') return;
213+
const identifier = tsNode; // narrowed
214+
215+
// ✅ Explanatory comment when guard is impractical
216+
// Safe: prefer-single-call only reports Identifier property nodes; computed access is excluded.
217+
const identifier = tsNode as TSESTree.Identifier;
218+
```
219+
220+
### Enumerate all upstream report sites before casting reportDescriptor.node
221+
222+
When writing a decorator, inspect **every** `context.report()` call in the upstream rule to determine all possible node types. A cast that only covers the common case will crash on edge cases.
223+
224+
```typescript
225+
// ❌ Assumes only one node type is ever reported
226+
const node = descriptor.node as TSESTree.TSCallSignatureDeclaration;
227+
228+
// ✅ After auditing all context.report() calls in the upstream source
229+
const node = descriptor.node as TSESTree.TSCallSignatureDeclaration | TSESTree.TSConstructSignatureDeclaration;
230+
```
231+
232+
This audit is the natural follow-on to Step 1 ("Find All Report Calls") in the FP tracing workflow above.
233+
234+
### Use specific node types in function signatures
235+
236+
Declare parameters with the most specific `estree`/`TSESTree` type the function actually requires. Using the base `estree.Node` hides the contract and disables type-checker assistance:
237+
238+
```typescript
239+
// ❌ Too broad
240+
function isInDirectionalContext(node: estree.Node): boolean { ... }
241+
242+
// ✅ Specific
243+
function isInDirectionalContext(node: estree.CallExpression): boolean { ... }
244+
```
245+
246+
---
247+
248+
## TypeScript Compiler API
249+
250+
### Use mutual assignability to test type equivalence
251+
252+
One-directional `isTypeAssignableTo(A, B)` only proves that `A` is a structural subtype of `B`. It returns `true` even when `B` is much wider, causing wrong-trigger suppressions.
253+
254+
```typescript
255+
// ❌ One-directional — false matches when B has extra optional fields
256+
if (checker.isTypeAssignableTo(typeA, typeB)) { suppress(); }
257+
258+
// ✅ Mutual — true only when both types are structurally equivalent
259+
if (checker.isTypeAssignableTo(typeA, typeB) && checker.isTypeAssignableTo(typeB, typeA)) {
260+
suppress();
261+
}
262+
```
263+
264+
### Use bitwise operators for TypeFlags comparisons
265+
266+
`ts.TypeFlags` values are bitmasks. A type can have multiple flags set simultaneously. Using `===` against a single flag value fails whenever any other flag is also set:
267+
268+
```typescript
269+
// ❌ Fails when multiple flags are set (common with union types)
270+
if (type.flags === ts.TypeFlags.Any) { ... }
271+
272+
// ✅ Bitwise mask — true whenever the flag is set, regardless of other flags
273+
if (type.flags & ts.TypeFlags.Any) { ... }
274+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { ... }
275+
```
276+
277+
---
278+
279+
## FP Remediation Quality
280+
281+
### Do not suppress reports that the rule is correct to raise
282+
283+
Not every complaint is a genuine false positive. Two cases to watch for:
284+
285+
**strictNullChecks disabled:** Do not add an exemption that suppresses non-null assertion (`!`) reports when `strictNullChecks` is off. Without `strictNullChecks`, `!` is a no-op — but suppressing the report hides future migration debt. Let the rule fire and guide developers to write proper null guards.
286+
287+
**Rule goal conflicts:** Before implementing an exception, re-read the upstream rule's documentation. If the exception would allow exactly the pattern the rule was designed to catch, the report is not a false positive. Do not proceed with the fix; flag it for human review instead.
288+
289+
### Cover the full API family when adding an exception
290+
291+
When adding a suppression exception for one method, identify all semantically equivalent siblings in the same API family and include them in the same guard:
292+
293+
```typescript
294+
// ❌ Only covers Object.keys — Object.getOwnPropertyNames users still get FPs
295+
if (isCallingMethod(node, 1, 'keys')) { suppress(); }
296+
297+
// ✅ Covers the full family
298+
if (isCallingMethod(node, 1, 'keys', 'getOwnPropertyNames', 'getOwnPropertySymbols')) {
299+
suppress();
300+
}
301+
```
302+
303+
Add test cases and update rule documentation for each covered variant.
304+
305+
---
306+
307+
## Decorator Design
308+
309+
### When all identification strategies fail, preserve the report
310+
311+
When a decorator uses multiple strategies to identify the owning component, and all strategies fail, preserve the original report. Do not fall back to `context.sourceCode.ast` (the entire file AST) as a catch-all scope:
312+
313+
```typescript
314+
// ❌ File-scope fallback — suppresses reports the decorator cannot correctly attribute
315+
const scope = findOwner(node) ?? context.sourceCode.ast;
316+
if (isSuppressible(node, scope)) return;
317+
318+
// ✅ No owner found → preserve the report
319+
const owner = findOwner(node);
320+
if (!owner) {
321+
context.report(reportDescriptor); // original report is more likely correct
322+
return;
323+
}
324+
```
325+
326+
### Scope suppression decisions to the component, not the file
327+
328+
Never cache a suppression decision at the file level. If one component matches, a file-level cache incorrectly suppresses reports for other components in the same file:
329+
330+
```typescript
331+
// ❌ File-level cache — contaminates all components in the file
332+
let fileHasSuppressibleProps = false;
333+
return {
334+
'Program:exit'() {
335+
if (fileHasSuppressibleProps) return; // suppresses everything in file
336+
}
337+
};
338+
339+
// ✅ Per-report decision scoped to the component
340+
(context, descriptor) => {
341+
const owner = findOwner(descriptor.node);
342+
if (owner && isSuppressible(descriptor.node, owner)) return;
343+
context.report(descriptor);
344+
}
345+
```
346+
347+
### Resolve spread expressions before suppressing
348+
349+
Do not suppress a report simply because a JSX spread attribute `{...expr}` is present. Attempt to resolve `expr` statically first:
350+
351+
```typescript
352+
// ❌ Suppresses on mere presence of spread — may produce false negatives
353+
if (node.openingElement.attributes.some(a => a.type === 'JSXSpreadAttribute')) return;
354+
355+
// ✅ Resolve the spread value first
356+
const spreadAttr = node.openingElement.attributes.find(a => a.type === 'JSXSpreadAttribute');
357+
if (spreadAttr) {
358+
const value = getValueOfExpression(context, spreadAttr.argument, 'ObjectExpression');
359+
if (!value) return; // unresolvable — suppress conservatively
360+
if (hasRelevantProperties(value)) return; // spread carries the relevant content
361+
// else: spread resolves to irrelevant object — do not suppress
362+
}
363+
```
364+
365+
### Document multi-strategy decorator helpers with JSDoc and inline comments
366+
367+
When a decorator helper uses multiple identification strategies, add JSDoc describing the strategy sequence and inline comments at each branch:
368+
369+
```typescript
370+
/**
371+
* Finds the React component that owns the reported props interface.
372+
* Strategy A: match by component name in the same file.
373+
* Strategy B: match by structural subtyping against known prop shapes.
374+
* Strategy C: match by mutual type assignability.
375+
* Returns null if no owner is found; the caller must preserve the report in that case.
376+
*/
377+
function findOwner(node: TSESTree.Node): ComponentNode | null {
378+
// Strategy A: direct name match
379+
const byName = findByName(node);
380+
if (byName) return byName;
381+
382+
// Strategy B: structural subtype check (faster, less precise)
383+
const byShape = findByShape(node);
384+
if (byShape) return byShape;
385+
386+
// Strategy C: mutual assignability (slower, most precise)
387+
return findByMutualAssignability(node);
388+
}
389+
```
390+
391+
---
392+
393+
## Code Style
394+
395+
### Use optional chaining for optional array length checks
396+
397+
```typescript
398+
// ❌ Redundant double guard
399+
if (arr && arr.length > 0) { ... }
400+
if (declaration?.typeParameters && declaration.typeParameters.length > 0) { ... }
401+
402+
// ✅ Optional chaining short-circuits on undefined; undefined > 0 is false
403+
if (arr?.length > 0) { ... }
404+
if (declaration?.typeParameters?.length > 0) { ... }
405+
```

0 commit comments

Comments
 (0)