Skip to content

Commit e17e0b1

Browse files
committed
chore(sync): cascade fleet template@4c380a7
Auto-applied by socket-wheelhouse sync-scaffolding into socket-registry. 3 file(s) touched: - .config/oxlint-plugin/lib/iterable-kind.mts - .config/oxlint-plugin/rules/no-cached-for-on-iterable.mts - .config/oxlint-plugin/rules/prefer-cached-for-loop.mts
1 parent 9924ee3 commit e17e0b1

3 files changed

Lines changed: 250 additions & 192 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* @fileoverview Shared "is this binding a Set / Map / Iterable?"
3+
* heuristic used by no-cached-for-on-iterable AND by
4+
* prefer-cached-for-loop's skip list.
5+
*
6+
* Without TypeScript type info available to oxlint plugins, the
7+
* detection is AST-only:
8+
*
9+
* - `new Set(...)` / `new Map(...)` / `new WeakSet(...)` /
10+
* `new WeakMap(...)` initializer → set/map
11+
* - `: Set<...>` / `: ReadonlySet<...>` / `: Map<...>` /
12+
* `: ReadonlyMap<...>` / `: WeakSet<...>` / `: WeakMap<...>`
13+
* annotation → set/map
14+
* - `: Iterable<...>` / `: AsyncIterable<...>` /
15+
* `: IterableIterator<...>` annotation → iterable
16+
* - `[…]` array literal / `: T[]` / `: Array<...>` /
17+
* `: ReadonlyArray<...>` / `Array.from(...)` / `Array.of(...)` /
18+
* `Object.keys|values|entries(...)` → array (negative signal)
19+
* - anything else → unknown (caller decides whether to skip)
20+
*
21+
* Two rules consume this:
22+
*
23+
* 1. `no-cached-for-on-iterable` — flags when a cached-length
24+
* `for (let i = 0, { length } = X; …)` loop is applied to a
25+
* set / map / iterable.
26+
*
27+
* 2. `prefer-cached-for-loop` — needs to SKIP rewriting
28+
* `for (const item of setVar)` into the cached-length shape,
29+
* because doing so produces the silent-no-op bug the other
30+
* rule catches. Without this skip, the two rules race each
31+
* other and the autofix re-introduces the bug.
32+
*
33+
* Both rules register the same visitors (`VariableDeclarator`,
34+
* `FunctionDeclaration`, `FunctionExpression`,
35+
* `ArrowFunctionExpression`) and share the resulting per-file map
36+
* via the helpers in this module.
37+
*/
38+
39+
import type { AstNode } from './rule-types.mts'
40+
41+
const SET_TYPE_NAMES = new Set(['Set', 'ReadonlySet', 'WeakSet'])
42+
const MAP_TYPE_NAMES = new Set(['Map', 'ReadonlyMap', 'WeakMap'])
43+
const ITERABLE_TYPE_NAMES = new Set([
44+
'Iterable',
45+
'AsyncIterable',
46+
'IterableIterator',
47+
])
48+
const ARRAY_TYPE_NAMES = new Set(['Array', 'ReadonlyArray'])
49+
50+
export type Kind = 'set' | 'map' | 'iterable' | 'array' | 'unknown'
51+
52+
// Non-array kinds — the ones flagged by no-cached-for-on-iterable
53+
// and the ones prefer-cached-for-loop must skip.
54+
export const FLAGGED_KINDS: ReadonlySet<Kind> = new Set([
55+
'set',
56+
'map',
57+
'iterable',
58+
])
59+
60+
/**
61+
* Classify a TS type-annotation AST node (the `: T` part of a
62+
* binding). Returns the kind, or `'unknown'` if the annotation is
63+
* absent or doesn't match a recognized shape. Shallow-only — does
64+
* NOT unwrap `Promise<Set<…>>` (returns unknown, which is safe).
65+
*/
66+
export function classifyTypeAnnotation(
67+
annotation: AstNode | undefined,
68+
): Kind {
69+
if (!annotation || !annotation.typeAnnotation) {
70+
return 'unknown'
71+
}
72+
const t = annotation.typeAnnotation
73+
if (t.type === 'TSArrayType') {
74+
return 'array'
75+
}
76+
if (t.type === 'TSTypeReference') {
77+
const name =
78+
t.typeName && t.typeName.type === 'Identifier'
79+
? t.typeName.name
80+
: undefined
81+
if (!name) {
82+
return 'unknown'
83+
}
84+
if (SET_TYPE_NAMES.has(name)) {
85+
return 'set'
86+
}
87+
if (MAP_TYPE_NAMES.has(name)) {
88+
return 'map'
89+
}
90+
if (ITERABLE_TYPE_NAMES.has(name)) {
91+
return 'iterable'
92+
}
93+
if (ARRAY_TYPE_NAMES.has(name)) {
94+
return 'array'
95+
}
96+
}
97+
return 'unknown'
98+
}
99+
100+
/**
101+
* Classify the initializer expression a VariableDeclarator is bound
102+
* to. Recognizes `new Set(...)` / `new Map(...)` and a handful of
103+
* array-materializing calls (`Array.from`, `Object.keys`, etc.) so
104+
* the rule doesn't fire on post-fix `const arr = Array.from(set)`
105+
* shapes.
106+
*/
107+
export function classifyInit(init: AstNode | undefined): Kind {
108+
if (!init) {
109+
return 'unknown'
110+
}
111+
if (init.type === 'ArrayExpression') {
112+
return 'array'
113+
}
114+
if (init.type === 'NewExpression' && init.callee.type === 'Identifier') {
115+
const name = init.callee.name as string
116+
if (SET_TYPE_NAMES.has(name)) {
117+
return 'set'
118+
}
119+
if (MAP_TYPE_NAMES.has(name)) {
120+
return 'map'
121+
}
122+
if (ARRAY_TYPE_NAMES.has(name)) {
123+
return 'array'
124+
}
125+
}
126+
if (
127+
init.type === 'CallExpression' &&
128+
init.callee.type === 'MemberExpression' &&
129+
init.callee.object.type === 'Identifier' &&
130+
!init.callee.computed &&
131+
init.callee.property.type === 'Identifier'
132+
) {
133+
const objName = init.callee.object.name as string
134+
const propName = init.callee.property.name as string
135+
if (objName === 'Array' && (propName === 'from' || propName === 'of')) {
136+
return 'array'
137+
}
138+
if (
139+
objName === 'Object' &&
140+
(propName === 'keys' || propName === 'values' || propName === 'entries')
141+
) {
142+
return 'array'
143+
}
144+
}
145+
return 'unknown'
146+
}
147+
148+
/**
149+
* Wire the per-file kind-tracking visitors into a rule's visitor
150+
* map. Returns the kinds Map and a record of visitor handlers the
151+
* caller should merge into its own visitor return. Use:
152+
*
153+
* const { kinds, visitors } = trackKinds()
154+
* return {
155+
* ...visitors,
156+
* ForStatement(node) { … kinds.get(name) … },
157+
* }
158+
*/
159+
export function trackKinds(): {
160+
kinds: Map<string, Kind>
161+
visitors: Record<string, (node: AstNode) => void>
162+
} {
163+
const kinds = new Map<string, Kind>()
164+
165+
function record(name: string | undefined, kind: Kind): void {
166+
if (!name || kind === 'unknown') {
167+
return
168+
}
169+
kinds.set(name, kind)
170+
}
171+
172+
function recordParams(params: AstNode[] | undefined): void {
173+
if (!params) {
174+
return
175+
}
176+
for (let i = 0, { length } = params; i < length; i += 1) {
177+
const p = params[i]
178+
if (!p || p.type !== 'Identifier') {
179+
continue
180+
}
181+
const name = p.name as string
182+
record(name, classifyTypeAnnotation(p.typeAnnotation))
183+
}
184+
}
185+
186+
return {
187+
kinds,
188+
visitors: {
189+
VariableDeclarator(node: AstNode) {
190+
if (!node.id || node.id.type !== 'Identifier') {
191+
return
192+
}
193+
const name = node.id.name as string
194+
const annotated = classifyTypeAnnotation(node.id.typeAnnotation)
195+
if (annotated !== 'unknown') {
196+
record(name, annotated)
197+
return
198+
}
199+
record(name, classifyInit(node.init))
200+
},
201+
FunctionDeclaration(node: AstNode) {
202+
recordParams(node.params)
203+
},
204+
FunctionExpression(node: AstNode) {
205+
recordParams(node.params)
206+
},
207+
ArrowFunctionExpression(node: AstNode) {
208+
recordParams(node.params)
209+
},
210+
},
211+
}
212+
}

0 commit comments

Comments
 (0)