Skip to content

Commit adcf94f

Browse files
committed
fix(audits): exempt only the imported OpenTelemetry context from the .with rule; unwrap cast partialize
1 parent b4645ea commit adcf94f

4 files changed

Lines changed: 55 additions & 16 deletions

File tree

‎.claude/rules/sim-react-performance.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ return items.sort(compare)
7777
return [...items].sort(compare)
7878
```
7979

80-
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except an OpenTelemetry context object (a receiver named like `context`, `otelContext`, `otelContextApi`); a deliberate exception carries `// utils-lint-allow: <reason>`.
80+
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except OpenTelemetry's context API (the `context` export of `@opentelemetry/api`, under any alias or via a namespace import); a deliberate exception carries `// utils-lint-allow: <reason>`.
8181

8282
## Run independent awaits in parallel
8383

‎.cursor/rules/sim-react-performance.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ return items.sort(compare)
8080
return [...items].sort(compare)
8181
```
8282

83-
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except an OpenTelemetry context object (a receiver named like `context`, `otelContext`, `otelContextApi`); a deliberate exception carries `// utils-lint-allow: <reason>`.
83+
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except OpenTelemetry's context API (the `context` export of `@opentelemetry/api`, under any alias or via a namespace import); a deliberate exception carries `// utils-lint-allow: <reason>`.
8484

8585
## Run independent awaits in parallel
8686

‎scripts/check-utils-enforcement.ts‎

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,6 @@ const BANNED_PATTERNS: Array<{
176176
/** Cheap gate: only files that contain a `.with(` call are parsed. */
177177
const WITH_CALL = /\.with\s*\(/
178178

179-
/** OpenTelemetry's `context.with(ctx, fn)` shares the shape; its receivers are named for the context API. */
180-
const OTEL_CONTEXT_RECEIVER = /context/i
181-
182179
/** A Babel AST node, read structurally rather than through `@babel/types`. */
183180
interface SyntaxNode extends Record<string, unknown> {
184181
type: string
@@ -200,19 +197,58 @@ function walkNodes(node: SyntaxNode, visit: (node: SyntaxNode) => void): void {
200197
}
201198
}
202199

203-
/** The name a member call is made on: `arr` in `arr.with(…)`, `items` in `this.items.with(…)`. */
204-
function receiverName(receiver: unknown): string | undefined {
205-
if (!isSyntaxNode(receiver)) return undefined
206-
if (receiver.type === 'Identifier' && typeof receiver.name === 'string') return receiver.name
207-
const isMember =
208-
receiver.type === 'MemberExpression' || receiver.type === 'OptionalMemberExpression'
209-
if (!isMember || receiver.computed === true || !isSyntaxNode(receiver.property)) return undefined
210-
return typeof receiver.property.name === 'string' ? receiver.property.name : undefined
200+
/**
201+
* Local names bound to OpenTelemetry's context API, whose `context.with(ctx, fn)` shares the
202+
* array method's shape: `context` (or an alias) imported from `@opentelemetry/api`, and any
203+
* namespace import of it (whose `.context` member is the same object).
204+
*/
205+
function otelContextBindings(program: SyntaxNode): {
206+
contexts: Set<string>
207+
namespaces: Set<string>
208+
} {
209+
const contexts = new Set<string>()
210+
const namespaces = new Set<string>()
211+
const body = Array.isArray(program.body) ? program.body : []
212+
for (const statement of body) {
213+
if (!isSyntaxNode(statement) || statement.type !== 'ImportDeclaration') continue
214+
if (!isSyntaxNode(statement.source) || statement.source.value !== '@opentelemetry/api') continue
215+
for (const specifier of Array.isArray(statement.specifiers) ? statement.specifiers : []) {
216+
if (!isSyntaxNode(specifier) || !isSyntaxNode(specifier.local)) continue
217+
const local = specifier.local.name
218+
if (typeof local !== 'string') continue
219+
if (specifier.type === 'ImportNamespaceSpecifier') namespaces.add(local)
220+
else if (
221+
specifier.type === 'ImportSpecifier' &&
222+
isSyntaxNode(specifier.imported) &&
223+
specifier.imported.name === 'context'
224+
)
225+
contexts.add(local)
226+
}
227+
}
228+
return { contexts, namespaces }
229+
}
230+
231+
/** Whether `receiver` is OpenTelemetry's context object: `context`, an alias, or `api.context`. */
232+
function isOtelContext(
233+
receiver: unknown,
234+
bindings: { contexts: Set<string>; namespaces: Set<string> }
235+
): boolean {
236+
if (!isSyntaxNode(receiver)) return false
237+
if (receiver.type === 'Identifier') return bindings.contexts.has(String(receiver.name))
238+
return (
239+
receiver.type === 'MemberExpression' &&
240+
receiver.computed !== true &&
241+
isSyntaxNode(receiver.object) &&
242+
receiver.object.type === 'Identifier' &&
243+
bindings.namespaces.has(String(receiver.object.name)) &&
244+
isSyntaxNode(receiver.property) &&
245+
receiver.property.name === 'context'
246+
)
211247
}
212248

213249
/**
214250
* Offsets of every `Array.prototype.with(index, value)` call: a two-argument `.with` on any
215-
* receiver except an OpenTelemetry context object. Drizzle's one-argument `.with(cte)` and
251+
* receiver except OpenTelemetry's context API (resolved through its `@opentelemetry/api` import). Drizzle's one-argument `.with(cte)` and
216252
* `index().with({ … })` never match.
217253
*/
218254
function findArrayWithCalls(file: string, content: string): number[] {
@@ -228,6 +264,7 @@ function findArrayWithCalls(file: string, content: string): number[] {
228264
}
229265
if (!isSyntaxNode(program)) return []
230266

267+
const bindings = otelContextBindings(program)
231268
const offsets: number[] = []
232269
walkNodes(program, (node) => {
233270
if (node.type !== 'CallExpression' && node.type !== 'OptionalCallExpression') return
@@ -236,7 +273,7 @@ function findArrayWithCalls(file: string, content: string): number[] {
236273
if (!isSyntaxNode(callee) || callee.computed === true || !isSyntaxNode(callee.property)) return
237274
if (callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') return
238275
if (callee.property.name !== 'with') return
239-
if (OTEL_CONTEXT_RECEIVER.test(receiverName(callee.object) ?? '')) return
276+
if (isOtelContext(callee.object, bindings)) return
240277
offsets.push(callee.property.start)
241278
})
242279
return offsets

‎scripts/check-zustand-v5-selectors.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,9 @@ function findPartialize(options: SyntaxNode): SyntaxNode | null {
410410
const keyName = isSyntaxNode(key) ? (key.type === 'Identifier' ? key.name : key.value) : null
411411
if (keyName !== 'partialize') continue
412412
if (property.type === 'ObjectMethod') return property
413-
return isSyntaxNode(property.value) ? property.value : null
413+
// A cast such as `((s) => s) as Partialize<S>` still hands zustand the inner function.
414+
const value = unwrapExpression(property.value)
415+
return isSyntaxNode(value) ? value : null
414416
}
415417
return null
416418
}

0 commit comments

Comments
 (0)