@pyreon/compiler 0.24.5 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -13
- package/src/defer-inline.ts +0 -686
- package/src/event-names.ts +0 -65
- package/src/index.ts +0 -61
- package/src/island-audit.ts +0 -675
- package/src/jsx.ts +0 -2792
- package/src/load-native.ts +0 -156
- package/src/lpih.ts +0 -270
- package/src/manifest.ts +0 -280
- package/src/project-scanner.ts +0 -214
- package/src/pyreon-intercept.ts +0 -1029
- package/src/react-intercept.ts +0 -1217
- package/src/reactivity-lens.ts +0 -190
- package/src/ssg-audit.ts +0 -513
- package/src/test-audit.ts +0 -435
- package/src/tests/backend-parity-r7-r9.test.ts +0 -91
- package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
- package/src/tests/collapse-bail-census.test.ts +0 -330
- package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
- package/src/tests/component-child-no-wrap.test.ts +0 -204
- package/src/tests/defer-inline.test.ts +0 -387
- package/src/tests/depth-stress.test.ts +0 -16
- package/src/tests/detector-tag-consistency.test.ts +0 -101
- package/src/tests/dynamic-collapse-detector.test.ts +0 -164
- package/src/tests/dynamic-collapse-emit.test.ts +0 -192
- package/src/tests/dynamic-collapse-scan.test.ts +0 -111
- package/src/tests/element-valued-const-child.test.ts +0 -61
- package/src/tests/falsy-child-characterization.test.ts +0 -48
- package/src/tests/island-audit.test.ts +0 -524
- package/src/tests/jsx.test.ts +0 -2908
- package/src/tests/load-native.test.ts +0 -53
- package/src/tests/lpih.test.ts +0 -404
- package/src/tests/malformed-input-resilience.test.ts +0 -50
- package/src/tests/manifest-snapshot.test.ts +0 -55
- package/src/tests/native-equivalence.test.ts +0 -924
- package/src/tests/partial-collapse-detector.test.ts +0 -121
- package/src/tests/partial-collapse-emit.test.ts +0 -104
- package/src/tests/partial-collapse-robustness.test.ts +0 -53
- package/src/tests/project-scanner.test.ts +0 -269
- package/src/tests/prop-derived-shadow.test.ts +0 -96
- package/src/tests/pure-call-reactive-args.test.ts +0 -50
- package/src/tests/pyreon-intercept.test.ts +0 -816
- package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
- package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
- package/src/tests/r15-elemconst-propderived.test.ts +0 -47
- package/src/tests/r19-defer-inline-robust.test.ts +0 -54
- package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
- package/src/tests/react-intercept.test.ts +0 -1104
- package/src/tests/reactivity-lens.test.ts +0 -170
- package/src/tests/rocketstyle-collapse.test.ts +0 -208
- package/src/tests/runtime/control-flow.test.ts +0 -159
- package/src/tests/runtime/dom-properties.test.ts +0 -138
- package/src/tests/runtime/events.test.ts +0 -301
- package/src/tests/runtime/harness.ts +0 -94
- package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
- package/src/tests/runtime/reactive-props.test.ts +0 -81
- package/src/tests/runtime/signals.test.ts +0 -129
- package/src/tests/runtime/whitespace.test.ts +0 -106
- package/src/tests/signal-autocall-shadow.test.ts +0 -86
- package/src/tests/sourcemap-fidelity.test.ts +0 -77
- package/src/tests/ssg-audit.test.ts +0 -402
- package/src/tests/static-text-baking.test.ts +0 -64
- package/src/tests/test-audit.test.ts +0 -549
- package/src/tests/transform-state-isolation.test.ts +0 -49
|
@@ -1,816 +0,0 @@
|
|
|
1
|
-
import { detectPyreonPatterns, hasPyreonPatterns } from '../pyreon-intercept'
|
|
2
|
-
|
|
3
|
-
describe('detectPyreonPatterns', () => {
|
|
4
|
-
describe('for-missing-by', () => {
|
|
5
|
-
it('flags <For each={...}> without a `by` prop', () => {
|
|
6
|
-
const code = `
|
|
7
|
-
const items = signal([1, 2, 3])
|
|
8
|
-
const UI = () => <For each={items()}>{(n) => <li>{n}</li>}</For>
|
|
9
|
-
`
|
|
10
|
-
const diags = detectPyreonPatterns(code)
|
|
11
|
-
expect(diags).toHaveLength(1)
|
|
12
|
-
expect(diags[0]!.code).toBe('for-missing-by')
|
|
13
|
-
expect(diags[0]!.message).toContain('keyed reconciler')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('does NOT flag <For> that carries a `by`', () => {
|
|
17
|
-
const code = `
|
|
18
|
-
const UI = () => <For each={items()} by={(i) => i.id}>{(i) => <li>{i.name}</li>}</For>
|
|
19
|
-
`
|
|
20
|
-
const diags = detectPyreonPatterns(code)
|
|
21
|
-
expect(diags.filter((d) => d.code === 'for-missing-by')).toEqual([])
|
|
22
|
-
})
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
describe('for-with-key', () => {
|
|
26
|
-
it('flags <For key={...}> as the wrong keying prop', () => {
|
|
27
|
-
const code = `
|
|
28
|
-
const UI = () => <For each={items()} key={(i) => i.id}>{(i) => <li>{i.name}</li>}</For>
|
|
29
|
-
`
|
|
30
|
-
const diags = detectPyreonPatterns(code)
|
|
31
|
-
const withKey = diags.find((d) => d.code === 'for-with-key')
|
|
32
|
-
expect(withKey).toBeDefined()
|
|
33
|
-
expect(withKey!.suggested).toContain('by={')
|
|
34
|
-
// fixable stays `false` until a `migrate_pyreon` tool ships; see
|
|
35
|
-
// the top-of-file note on detectPyreonPatterns.
|
|
36
|
-
expect(withKey!.fixable).toBe(false)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('does not ALSO flag for-missing-by when for-with-key fires', () => {
|
|
40
|
-
// Otherwise consumers would see two entries for the same mistake.
|
|
41
|
-
const code = `<For each={items} key={(i) => i.id}>{(i) => <li />}</For>`
|
|
42
|
-
const diags = detectPyreonPatterns(code)
|
|
43
|
-
expect(diags.filter((d) => d.code === 'for-missing-by')).toEqual([])
|
|
44
|
-
expect(diags.filter((d) => d.code === 'for-with-key')).toHaveLength(1)
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('props-destructured', () => {
|
|
49
|
-
it('flags destructured props on arrow component functions', () => {
|
|
50
|
-
const code = `
|
|
51
|
-
const Greeting = ({ name }: { name: string }) => <div>Hello {name}</div>
|
|
52
|
-
`
|
|
53
|
-
const diags = detectPyreonPatterns(code)
|
|
54
|
-
expect(diags).toHaveLength(1)
|
|
55
|
-
expect(diags[0]!.code).toBe('props-destructured')
|
|
56
|
-
expect(diags[0]!.message).toContain('ONCE')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('flags destructured props on function declarations that render JSX', () => {
|
|
60
|
-
const code = `
|
|
61
|
-
function Greeting({ name }: { name: string }) {
|
|
62
|
-
return <div>Hello {name}</div>
|
|
63
|
-
}
|
|
64
|
-
`
|
|
65
|
-
const diags = detectPyreonPatterns(code)
|
|
66
|
-
expect(diags.filter((d) => d.code === 'props-destructured')).toHaveLength(1)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('does NOT flag destructured params on non-component callbacks', () => {
|
|
70
|
-
const code = `
|
|
71
|
-
const handler = ({ value }: { value: string }) => console.log(value)
|
|
72
|
-
const reduce = ({ a, b }: { a: number; b: number }) => a + b
|
|
73
|
-
`
|
|
74
|
-
const diags = detectPyreonPatterns(code)
|
|
75
|
-
expect(diags.filter((d) => d.code === 'props-destructured')).toEqual([])
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('does NOT flag components that accept a single `props` parameter', () => {
|
|
79
|
-
const code = `
|
|
80
|
-
const Greeting = (props: { name: string }) => <div>Hello {props.name}</div>
|
|
81
|
-
`
|
|
82
|
-
const diags = detectPyreonPatterns(code)
|
|
83
|
-
expect(diags.filter((d) => d.code === 'props-destructured')).toEqual([])
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('props-destructured-body', () => {
|
|
88
|
-
const only = (code: string) =>
|
|
89
|
-
detectPyreonPatterns(code).filter((d) => d.code === 'props-destructured-body')
|
|
90
|
-
|
|
91
|
-
it('flags `const { x } = props` in an arrow component body', () => {
|
|
92
|
-
const code = `
|
|
93
|
-
const Greeting = (props: { name: string }) => {
|
|
94
|
-
const { name } = props
|
|
95
|
-
return <div>Hello {name}</div>
|
|
96
|
-
}
|
|
97
|
-
`
|
|
98
|
-
const diags = only(code)
|
|
99
|
-
expect(diags).toHaveLength(1)
|
|
100
|
-
expect(diags[0]!.code).toBe('props-destructured-body')
|
|
101
|
-
expect(diags[0]!.message).toContain('ONCE')
|
|
102
|
-
expect(diags[0]!.fixable).toBe(false)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('flags it in a function-declaration component', () => {
|
|
106
|
-
const code = `
|
|
107
|
-
function Greeting(props: { name: string }) {
|
|
108
|
-
const { name } = props
|
|
109
|
-
return <div>Hello {name}</div>
|
|
110
|
-
}
|
|
111
|
-
`
|
|
112
|
-
expect(only(code)).toHaveLength(1)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('flags let / var / alias / default / rest / nested shapes', () => {
|
|
116
|
-
const code = `
|
|
117
|
-
const A = (props: any) => { let { a } = props; return <i>{a}</i> }
|
|
118
|
-
const B = (props: any) => { var { b } = props; return <i>{b}</i> }
|
|
119
|
-
const C = (props: any) => { const { c: cc } = props; return <i>{cc}</i> }
|
|
120
|
-
const D = (props: any) => { const { d = 1 } = props; return <i>{d}</i> }
|
|
121
|
-
const E = (props: any) => { const { ...rest } = props; return <i>{rest.x}</i> }
|
|
122
|
-
const F = (props: any) => { const { f: { g } } = props; return <i>{g}</i> }
|
|
123
|
-
`
|
|
124
|
-
expect(only(code)).toHaveLength(6)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('flags a destructure nested inside a body-scope if-block (still synchronous)', () => {
|
|
128
|
-
const code = `
|
|
129
|
-
const Gate = (props: any) => {
|
|
130
|
-
if (props.cond) { const { x } = props; return <i>{x}</i> }
|
|
131
|
-
return <i />
|
|
132
|
-
}
|
|
133
|
-
`
|
|
134
|
-
expect(only(code)).toHaveLength(1)
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('unwraps `as` / `satisfies` / `!` / parens on the initializer', () => {
|
|
138
|
-
const code = `
|
|
139
|
-
const A = (props: any) => { const { a } = props as Props; return <i>{a}</i> }
|
|
140
|
-
const B = (props: any) => { const { b } = (props); return <i>{b}</i> }
|
|
141
|
-
const C = (props: any) => { const { c } = props!; return <i>{c}</i> }
|
|
142
|
-
const D = (props: any) => { const { d } = props satisfies Props; return <i>{d}</i> }
|
|
143
|
-
`
|
|
144
|
-
expect(only(code)).toHaveLength(4)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('does NOT flag `const x = props` (alias, no destructure)', () => {
|
|
148
|
-
const code = `
|
|
149
|
-
const Greeting = (props: any) => { const p = props; return <div>{p.name}</div> }
|
|
150
|
-
`
|
|
151
|
-
expect(only(code)).toEqual([])
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('does NOT flag a destructure off a non-props identifier', () => {
|
|
155
|
-
const code = `
|
|
156
|
-
const Greeting = (props: any) => {
|
|
157
|
-
const store = useStore()
|
|
158
|
-
const { name } = store
|
|
159
|
-
return <div>{name}{props.x}</div>
|
|
160
|
-
}
|
|
161
|
-
`
|
|
162
|
-
expect(only(code)).toEqual([])
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('does NOT flag `const { x } = props.nested` (member, out of canonical scope by design)', () => {
|
|
166
|
-
const code = `
|
|
167
|
-
const Greeting = (props: any) => { const { x } = props.nested; return <div>{x}</div> }
|
|
168
|
-
`
|
|
169
|
-
expect(only(code)).toEqual([])
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('does NOT flag destructures inside nested functions (handler / effect / returned accessor)', () => {
|
|
173
|
-
const code = `
|
|
174
|
-
const Handler = (props: any) => {
|
|
175
|
-
const onClick = () => { const { id } = props; doThing(id) }
|
|
176
|
-
effect(() => { const { y } = props; track(y) })
|
|
177
|
-
return <button onClick={onClick}>{() => { const { z } = props; return z }}</button>
|
|
178
|
-
}
|
|
179
|
-
`
|
|
180
|
-
expect(only(code)).toEqual([])
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('does NOT flag the returned reactive-accessor fix shape', () => {
|
|
184
|
-
const code = `
|
|
185
|
-
const Greeting = (props: any) =>
|
|
186
|
-
(() => { const { name } = props; return <div>Hello {name}</div> })
|
|
187
|
-
`
|
|
188
|
-
// The body is an arrow expression returning a nested accessor — the
|
|
189
|
-
// destructure lives inside the nested fn, which re-reads props.
|
|
190
|
-
expect(only(code)).toEqual([])
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('does NOT flag the parameter-destructure shape (props-destructured owns it)', () => {
|
|
194
|
-
const code = `
|
|
195
|
-
const Greeting = ({ name }: { name: string }) => <div>Hello {name}</div>
|
|
196
|
-
`
|
|
197
|
-
const diags = detectPyreonPatterns(code)
|
|
198
|
-
expect(diags.filter((d) => d.code === 'props-destructured-body')).toEqual([])
|
|
199
|
-
expect(diags.filter((d) => d.code === 'props-destructured')).toHaveLength(1)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('does NOT flag non-component helpers that destructure an arg named props', () => {
|
|
203
|
-
const code = `
|
|
204
|
-
function mergeProps(props: any) { const { a, b } = props; return a + b }
|
|
205
|
-
const reducer = (props: any) => { const { x } = props; return x }
|
|
206
|
-
`
|
|
207
|
-
expect(only(code)).toEqual([])
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('does NOT flag a lowercase JSX-returning function (not component-shaped)', () => {
|
|
211
|
-
const code = `
|
|
212
|
-
const renderRow = (props: any) => { const { cell } = props; return <td>{cell}</td> }
|
|
213
|
-
`
|
|
214
|
-
expect(only(code)).toEqual([])
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
describe('process-dev-gate', () => {
|
|
219
|
-
it('flags typeof process + NODE_ENV production gates', () => {
|
|
220
|
-
const code = `
|
|
221
|
-
if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
|
222
|
-
console.warn('dev only')
|
|
223
|
-
}
|
|
224
|
-
`
|
|
225
|
-
const diags = detectPyreonPatterns(code)
|
|
226
|
-
expect(diags).toHaveLength(1)
|
|
227
|
-
expect(diags[0]!.code).toBe('process-dev-gate')
|
|
228
|
-
expect(diags[0]!.suggested).toContain('import.meta.env')
|
|
229
|
-
expect(diags[0]!.fixable).toBe(false)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('flags the reversed operand order', () => {
|
|
233
|
-
const code = `
|
|
234
|
-
const IS_DEV = process.env.NODE_ENV !== 'production' && typeof process !== 'undefined'
|
|
235
|
-
`
|
|
236
|
-
const diags = detectPyreonPatterns(code)
|
|
237
|
-
expect(diags.filter((d) => d.code === 'process-dev-gate')).toHaveLength(1)
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('does NOT flag plain typeof process checks (server-side code is fine)', () => {
|
|
241
|
-
const code = `
|
|
242
|
-
if (typeof process !== 'undefined') {
|
|
243
|
-
process.exit(0)
|
|
244
|
-
}
|
|
245
|
-
`
|
|
246
|
-
const diags = detectPyreonPatterns(code)
|
|
247
|
-
expect(diags).toEqual([])
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
describe('empty-theme', () => {
|
|
252
|
-
it('flags `.theme({})` as a no-op chain', () => {
|
|
253
|
-
const code = `
|
|
254
|
-
const Button = rocketstyle('button').attrs({ tag: 'button' }).theme({})
|
|
255
|
-
`
|
|
256
|
-
const diags = detectPyreonPatterns(code)
|
|
257
|
-
expect(diags).toHaveLength(1)
|
|
258
|
-
expect(diags[0]!.code).toBe('empty-theme')
|
|
259
|
-
expect(diags[0]!.fixable).toBe(false)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
it('does NOT flag `.theme(...)` with actual content', () => {
|
|
263
|
-
const code = `
|
|
264
|
-
const Button = rocketstyle('button').theme({ color: 'red' })
|
|
265
|
-
`
|
|
266
|
-
const diags = detectPyreonPatterns(code)
|
|
267
|
-
expect(diags).toEqual([])
|
|
268
|
-
})
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
describe('raw-add-event-listener / raw-remove-event-listener', () => {
|
|
272
|
-
it('flags window.addEventListener with useEventListener suggestion', () => {
|
|
273
|
-
const code = `
|
|
274
|
-
const Panel = () => {
|
|
275
|
-
window.addEventListener('resize', () => console.log('resize'))
|
|
276
|
-
return <div />
|
|
277
|
-
}
|
|
278
|
-
`
|
|
279
|
-
const diags = detectPyreonPatterns(code)
|
|
280
|
-
const add = diags.find((d) => d.code === 'raw-add-event-listener')
|
|
281
|
-
expect(add).toBeDefined()
|
|
282
|
-
expect(add!.suggested).toContain('useEventListener')
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it('flags document.removeEventListener', () => {
|
|
286
|
-
const code = `
|
|
287
|
-
document.removeEventListener('click', handler)
|
|
288
|
-
`
|
|
289
|
-
const diags = detectPyreonPatterns(code)
|
|
290
|
-
expect(diags.find((d) => d.code === 'raw-remove-event-listener')).toBeDefined()
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('does NOT flag addEventListener on host-owned nested paths (e.g. editor.dom)', () => {
|
|
294
|
-
// `view.dom.ownerDocument...` and similar framework-host chains
|
|
295
|
-
// are intentional — the rule should only flag bare window/document
|
|
296
|
-
// and obvious DOM-element identifiers.
|
|
297
|
-
const code = `
|
|
298
|
-
view.dom.ownerDocument.addEventListener('click', h)
|
|
299
|
-
`
|
|
300
|
-
const diags = detectPyreonPatterns(code)
|
|
301
|
-
expect(diags).toEqual([])
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
describe('date-math-random-id', () => {
|
|
306
|
-
it('flags Date.now() + Math.random() ID patterns', () => {
|
|
307
|
-
const code = `
|
|
308
|
-
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
|
309
|
-
`
|
|
310
|
-
const diags = detectPyreonPatterns(code)
|
|
311
|
-
// The binary expression and any enclosing template expressions can both
|
|
312
|
-
// match — dedupe by line to keep the contract observable.
|
|
313
|
-
const atLine = [...new Set(diags.filter((d) => d.code === 'date-math-random-id').map((d) => d.line))]
|
|
314
|
-
expect(atLine.length).toBeGreaterThanOrEqual(1)
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('flags template-literal variants', () => {
|
|
318
|
-
const code = 'const id = `${Date.now()}-${Math.random()}`'
|
|
319
|
-
const diags = detectPyreonPatterns(code)
|
|
320
|
-
expect(diags.find((d) => d.code === 'date-math-random-id')).toBeDefined()
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
it('does NOT flag Date.now() alone', () => {
|
|
324
|
-
const code = `const now = Date.now()`
|
|
325
|
-
const diags = detectPyreonPatterns(code)
|
|
326
|
-
expect(diags).toEqual([])
|
|
327
|
-
})
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
describe('on-click-undefined', () => {
|
|
331
|
-
it('flags explicit onClick={undefined}', () => {
|
|
332
|
-
const code = `<button onClick={undefined}>Go</button>`
|
|
333
|
-
const diags = detectPyreonPatterns(code)
|
|
334
|
-
expect(diags).toHaveLength(1)
|
|
335
|
-
expect(diags[0]!.code).toBe('on-click-undefined')
|
|
336
|
-
expect(diags[0]!.fixable).toBe(false)
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('flags other on* handlers set to undefined', () => {
|
|
340
|
-
const code = `<input onInput={undefined} />`
|
|
341
|
-
const diags = detectPyreonPatterns(code)
|
|
342
|
-
expect(diags.find((d) => d.code === 'on-click-undefined')).toBeDefined()
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
it('does NOT flag onClick={cond ? handler : undefined} (conditional is safe)', () => {
|
|
346
|
-
const code = `<button onClick={condition ? handler : undefined}>Go</button>`
|
|
347
|
-
const diags = detectPyreonPatterns(code)
|
|
348
|
-
expect(diags.filter((d) => d.code === 'on-click-undefined')).toEqual([])
|
|
349
|
-
})
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
describe('hasPyreonPatterns (regex pre-filter)', () => {
|
|
353
|
-
it('returns true for every detected pattern', () => {
|
|
354
|
-
const samples = [
|
|
355
|
-
`<For each={x}>{(i) => <li />}</For>`,
|
|
356
|
-
`typeof process !== 'undefined'`,
|
|
357
|
-
`.theme({})`,
|
|
358
|
-
`window.addEventListener('x', h)`,
|
|
359
|
-
`const id = \`\${Date.now()}-\${Math.random()}\``,
|
|
360
|
-
`<button onClick={undefined}>x</button>`,
|
|
361
|
-
`const X = ({ name }) => <div>{name}</div>`,
|
|
362
|
-
]
|
|
363
|
-
for (const s of samples) {
|
|
364
|
-
expect(hasPyreonPatterns(s)).toBe(true)
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
it('returns false for unrelated code', () => {
|
|
369
|
-
expect(hasPyreonPatterns(`const x = 1 + 2`)).toBe(false)
|
|
370
|
-
expect(hasPyreonPatterns(`console.log('ok')`)).toBe(false)
|
|
371
|
-
})
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
describe('combined scenarios', () => {
|
|
375
|
-
it('finds every distinct pattern in a multi-issue file', () => {
|
|
376
|
-
const code = `
|
|
377
|
-
const List = ({ items }) => <For each={items}>{(i) => <li />}</For>
|
|
378
|
-
const flag = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
|
|
379
|
-
window.addEventListener('resize', () => {})
|
|
380
|
-
const Styled = rocketstyle('div').theme({})
|
|
381
|
-
const id = Date.now() + Math.random()
|
|
382
|
-
const Btn = () => <button onClick={undefined}>x</button>
|
|
383
|
-
`
|
|
384
|
-
const diags = detectPyreonPatterns(code)
|
|
385
|
-
const codes = new Set(diags.map((d) => d.code))
|
|
386
|
-
expect(codes.has('for-missing-by')).toBe(true)
|
|
387
|
-
expect(codes.has('props-destructured')).toBe(true)
|
|
388
|
-
expect(codes.has('process-dev-gate')).toBe(true)
|
|
389
|
-
expect(codes.has('raw-add-event-listener')).toBe(true)
|
|
390
|
-
expect(codes.has('empty-theme')).toBe(true)
|
|
391
|
-
expect(codes.has('date-math-random-id')).toBe(true)
|
|
392
|
-
expect(codes.has('on-click-undefined')).toBe(true)
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
it('returns an empty array for idiomatic Pyreon code', () => {
|
|
396
|
-
const code = `
|
|
397
|
-
import { signal, effect } from '@pyreon/reactivity'
|
|
398
|
-
import { useEventListener } from '@pyreon/hooks'
|
|
399
|
-
|
|
400
|
-
const Counter = (props: { initial?: number }) => {
|
|
401
|
-
const count = signal(props.initial ?? 0)
|
|
402
|
-
useEventListener(window, 'keydown', () => count.update((n) => n + 1))
|
|
403
|
-
return (
|
|
404
|
-
<For each={items()} by={(i) => i.id}>
|
|
405
|
-
{(i) => <li>{i.name}: {count()}</li>}
|
|
406
|
-
</For>
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
`
|
|
410
|
-
expect(detectPyreonPatterns(code)).toEqual([])
|
|
411
|
-
})
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
describe('fixable contract — ALL Pyreon codes are fixable:false', () => {
|
|
415
|
-
// Binding invariant: until a `migrate_pyreon` tool exists, every
|
|
416
|
-
// Pyreon diagnostic must report `fixable: false`. Claiming a code
|
|
417
|
-
// is auto-fixable while no migrator handles it would mislead
|
|
418
|
-
// consumers building on the flag. Flip to `true` only when the
|
|
419
|
-
// companion migrator lands in a subsequent PR.
|
|
420
|
-
it('never emits a Pyreon diagnostic with fixable: true', () => {
|
|
421
|
-
const snippets = [
|
|
422
|
-
`<For each={x} key={(i) => i.id}>{(i) => <li />}</For>`, // for-with-key
|
|
423
|
-
`<For each={x}>{(i) => <li />}</For>`, // for-missing-by
|
|
424
|
-
`const X = ({ y }) => <div>{y}</div>`, // props-destructured
|
|
425
|
-
`typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'`, // process-dev-gate
|
|
426
|
-
`rocketstyle('div').theme({})`, // empty-theme
|
|
427
|
-
`window.addEventListener('x', h)`, // raw-add-event-listener
|
|
428
|
-
`document.removeEventListener('x', h)`, // raw-remove-event-listener
|
|
429
|
-
'const id = `${Date.now()}-${Math.random()}`', // date-math-random-id
|
|
430
|
-
`<button onClick={undefined}>x</button>`, // on-click-undefined
|
|
431
|
-
]
|
|
432
|
-
for (const code of snippets) {
|
|
433
|
-
const diags = detectPyreonPatterns(code)
|
|
434
|
-
for (const d of diags) {
|
|
435
|
-
expect(d.fixable, `${d.code} must be fixable:false`).toBe(false)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
})
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
describe('diagnostic shape', () => {
|
|
442
|
-
it('emits 1-based line + 0-based column with trimmed current/suggested', () => {
|
|
443
|
-
const code = `\n\n <For each={items}>{(i) => <li />}</For>`
|
|
444
|
-
const diags = detectPyreonPatterns(code)
|
|
445
|
-
expect(diags[0]!.line).toBe(3)
|
|
446
|
-
expect(diags[0]!.column).toBeGreaterThanOrEqual(0)
|
|
447
|
-
expect(diags[0]!.current).not.toMatch(/^\s/)
|
|
448
|
-
expect(diags[0]!.suggested).not.toMatch(/^\s/)
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
it('sorts diagnostics by line ascending', () => {
|
|
452
|
-
const code = `
|
|
453
|
-
const A = () => <button onClick={undefined} />
|
|
454
|
-
const B = () => <For each={x} />
|
|
455
|
-
const C = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
|
|
456
|
-
`
|
|
457
|
-
const diags = detectPyreonPatterns(code)
|
|
458
|
-
const lines = diags.map((d) => d.line)
|
|
459
|
-
expect(lines).toEqual([...lines].sort((a, b) => a - b))
|
|
460
|
-
})
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
describe('signal-write-as-call', () => {
|
|
464
|
-
it('flags `sig(value)` when sig was declared as a signal', () => {
|
|
465
|
-
const code = `
|
|
466
|
-
import { signal } from '@pyreon/reactivity'
|
|
467
|
-
const count = signal(0)
|
|
468
|
-
function inc() { count(count() + 1) }
|
|
469
|
-
`
|
|
470
|
-
const diags = detectPyreonPatterns(code)
|
|
471
|
-
const hits = diags.filter((d) => d.code === 'signal-write-as-call')
|
|
472
|
-
expect(hits).toHaveLength(1)
|
|
473
|
-
expect(hits[0]!.message).toContain('signal()')
|
|
474
|
-
expect(hits[0]!.suggested).toContain('count.set(')
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
it('does NOT flag `sig()` (zero args — that is the read API)', () => {
|
|
478
|
-
const code = `
|
|
479
|
-
const count = signal(0)
|
|
480
|
-
function read() { return count() }
|
|
481
|
-
`
|
|
482
|
-
const diags = detectPyreonPatterns(code)
|
|
483
|
-
expect(diags.filter((d) => d.code === 'signal-write-as-call')).toEqual([])
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
it('does NOT flag `sig.set(value)` (the proper write API)', () => {
|
|
487
|
-
const code = `
|
|
488
|
-
const count = signal(0)
|
|
489
|
-
function set(v) { count.set(v) }
|
|
490
|
-
`
|
|
491
|
-
const diags = detectPyreonPatterns(code)
|
|
492
|
-
expect(diags.filter((d) => d.code === 'signal-write-as-call')).toEqual([])
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
it('does NOT flag calls on identifiers that are not signal-bound', () => {
|
|
496
|
-
const code = `
|
|
497
|
-
const handler = (v) => console.log(v)
|
|
498
|
-
handler(42)
|
|
499
|
-
`
|
|
500
|
-
const diags = detectPyreonPatterns(code)
|
|
501
|
-
expect(diags.filter((d) => d.code === 'signal-write-as-call')).toEqual([])
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
it('flags `computed(value)` shape too — same misread of the API', () => {
|
|
505
|
-
const code = `
|
|
506
|
-
const doubled = computed(() => count() * 2)
|
|
507
|
-
function bug() { doubled(99) }
|
|
508
|
-
`
|
|
509
|
-
const diags = detectPyreonPatterns(code)
|
|
510
|
-
expect(diags.filter((d) => d.code === 'signal-write-as-call')).toHaveLength(1)
|
|
511
|
-
})
|
|
512
|
-
})
|
|
513
|
-
|
|
514
|
-
describe('static-return-null-conditional', () => {
|
|
515
|
-
it('flags `if (cond) return null` at the top of a component body', () => {
|
|
516
|
-
const code = `
|
|
517
|
-
function TabPanel({ id }) {
|
|
518
|
-
if (!isActive(id)) return null
|
|
519
|
-
return <div class="panel">content</div>
|
|
520
|
-
}
|
|
521
|
-
`
|
|
522
|
-
const diags = detectPyreonPatterns(code)
|
|
523
|
-
const hits = diags.filter((d) => d.code === 'static-return-null-conditional')
|
|
524
|
-
expect(hits).toHaveLength(1)
|
|
525
|
-
expect(hits[0]!.message).toContain('run ONCE')
|
|
526
|
-
expect(hits[0]!.suggested).toContain('=> {')
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('flags the block-form `if (cond) { return null }` too', () => {
|
|
530
|
-
const code = `
|
|
531
|
-
function Modal() {
|
|
532
|
-
if (!isOpen()) {
|
|
533
|
-
return null
|
|
534
|
-
}
|
|
535
|
-
return <div class="modal">…</div>
|
|
536
|
-
}
|
|
537
|
-
`
|
|
538
|
-
const diags = detectPyreonPatterns(code)
|
|
539
|
-
expect(
|
|
540
|
-
diags.filter((d) => d.code === 'static-return-null-conditional'),
|
|
541
|
-
).toHaveLength(1)
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
it('does NOT flag non-component functions returning null', () => {
|
|
545
|
-
const code = `
|
|
546
|
-
function findUser(id) {
|
|
547
|
-
if (!id) return null
|
|
548
|
-
return { id }
|
|
549
|
-
}
|
|
550
|
-
`
|
|
551
|
-
const diags = detectPyreonPatterns(code)
|
|
552
|
-
expect(diags.filter((d) => d.code === 'static-return-null-conditional')).toEqual([])
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
it('does NOT flag the recommended reactive-accessor pattern', () => {
|
|
556
|
-
const code = `
|
|
557
|
-
function TabPanel() {
|
|
558
|
-
return (() => {
|
|
559
|
-
if (!isActive()) return null
|
|
560
|
-
return <div>content</div>
|
|
561
|
-
})
|
|
562
|
-
}
|
|
563
|
-
`
|
|
564
|
-
const diags = detectPyreonPatterns(code)
|
|
565
|
-
// The inner arrow contains the if-return-null but is itself a
|
|
566
|
-
// returned reactive accessor — not the "static-return-null" shape
|
|
567
|
-
// because the OUTER component's body has no top-level if-return-null.
|
|
568
|
-
expect(diags.filter((d) => d.code === 'static-return-null-conditional')).toEqual([])
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
it('only flags ONCE per component body even when chained', () => {
|
|
572
|
-
const code = `
|
|
573
|
-
function MultiGuard() {
|
|
574
|
-
if (!a()) return null
|
|
575
|
-
if (!b()) return null
|
|
576
|
-
return <div>ok</div>
|
|
577
|
-
}
|
|
578
|
-
`
|
|
579
|
-
const diags = detectPyreonPatterns(code)
|
|
580
|
-
expect(
|
|
581
|
-
diags.filter((d) => d.code === 'static-return-null-conditional'),
|
|
582
|
-
).toHaveLength(1)
|
|
583
|
-
})
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
describe('as-unknown-as-vnodechild', () => {
|
|
587
|
-
it('flags `expr as unknown as VNodeChild`', () => {
|
|
588
|
-
const code = `
|
|
589
|
-
function Wrapper() {
|
|
590
|
-
return (<div>hi</div> as unknown as VNodeChild)
|
|
591
|
-
}
|
|
592
|
-
`
|
|
593
|
-
const diags = detectPyreonPatterns(code)
|
|
594
|
-
const hits = diags.filter((d) => d.code === 'as-unknown-as-vnodechild')
|
|
595
|
-
expect(hits).toHaveLength(1)
|
|
596
|
-
expect(hits[0]!.message).toContain('JSX.Element')
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
it('does NOT flag a single `as VNodeChild` (no double-cast)', () => {
|
|
600
|
-
const code = `
|
|
601
|
-
function Wrapper() {
|
|
602
|
-
return (something as VNodeChild)
|
|
603
|
-
}
|
|
604
|
-
`
|
|
605
|
-
const diags = detectPyreonPatterns(code)
|
|
606
|
-
expect(diags.filter((d) => d.code === 'as-unknown-as-vnodechild')).toEqual([])
|
|
607
|
-
})
|
|
608
|
-
|
|
609
|
-
it('does NOT flag `as unknown as OtherType`', () => {
|
|
610
|
-
const code = `
|
|
611
|
-
const x = (foo as unknown as Whatever)
|
|
612
|
-
`
|
|
613
|
-
const diags = detectPyreonPatterns(code)
|
|
614
|
-
expect(diags.filter((d) => d.code === 'as-unknown-as-vnodechild')).toEqual([])
|
|
615
|
-
})
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
describe('island-never-with-registry-entry', () => {
|
|
619
|
-
it('flags a hydrateIslands key matching a hydrate: "never" island declaration', () => {
|
|
620
|
-
const code = `
|
|
621
|
-
import { island } from '@pyreon/server'
|
|
622
|
-
import { hydrateIslands } from '@pyreon/server/client'
|
|
623
|
-
export const StaticBadge = island(() => import('./StaticBadge'), {
|
|
624
|
-
name: 'StaticBadge',
|
|
625
|
-
hydrate: 'never',
|
|
626
|
-
})
|
|
627
|
-
hydrateIslands({
|
|
628
|
-
StaticBadge: () => import('./StaticBadge'),
|
|
629
|
-
})
|
|
630
|
-
`
|
|
631
|
-
const diags = detectPyreonPatterns(code)
|
|
632
|
-
const hits = diags.filter((d) => d.code === 'island-never-with-registry-entry')
|
|
633
|
-
expect(hits).toHaveLength(1)
|
|
634
|
-
expect(hits[0]!.message).toContain('StaticBadge')
|
|
635
|
-
expect(hits[0]!.message).toContain("'never'")
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
it('does NOT flag a hydrateIslands key for a non-never island', () => {
|
|
639
|
-
const code = `
|
|
640
|
-
import { island } from '@pyreon/server'
|
|
641
|
-
import { hydrateIslands } from '@pyreon/server/client'
|
|
642
|
-
export const Counter = island(() => import('./Counter'), {
|
|
643
|
-
name: 'Counter',
|
|
644
|
-
hydrate: 'load',
|
|
645
|
-
})
|
|
646
|
-
hydrateIslands({
|
|
647
|
-
Counter: () => import('./Counter'),
|
|
648
|
-
})
|
|
649
|
-
`
|
|
650
|
-
const diags = detectPyreonPatterns(code)
|
|
651
|
-
expect(diags.filter((d) => d.code === 'island-never-with-registry-entry')).toEqual([])
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
it('does NOT flag a never-island when no hydrateIslands call appears', () => {
|
|
655
|
-
const code = `
|
|
656
|
-
import { island } from '@pyreon/server'
|
|
657
|
-
export const StaticBadge = island(() => import('./StaticBadge'), {
|
|
658
|
-
name: 'StaticBadge',
|
|
659
|
-
hydrate: 'never',
|
|
660
|
-
})
|
|
661
|
-
`
|
|
662
|
-
const diags = detectPyreonPatterns(code)
|
|
663
|
-
expect(diags.filter((d) => d.code === 'island-never-with-registry-entry')).toEqual([])
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
it('does NOT flag when hydrateIslands omits never-strategy islands (canonical)', () => {
|
|
667
|
-
const code = `
|
|
668
|
-
import { island } from '@pyreon/server'
|
|
669
|
-
import { hydrateIslands } from '@pyreon/server/client'
|
|
670
|
-
export const Counter = island(() => import('./Counter'), {
|
|
671
|
-
name: 'Counter',
|
|
672
|
-
hydrate: 'load',
|
|
673
|
-
})
|
|
674
|
-
export const StaticBadge = island(() => import('./StaticBadge'), {
|
|
675
|
-
name: 'StaticBadge',
|
|
676
|
-
hydrate: 'never',
|
|
677
|
-
})
|
|
678
|
-
hydrateIslands({
|
|
679
|
-
Counter: () => import('./Counter'),
|
|
680
|
-
// StaticBadge intentionally omitted
|
|
681
|
-
})
|
|
682
|
-
`
|
|
683
|
-
const diags = detectPyreonPatterns(code)
|
|
684
|
-
expect(diags.filter((d) => d.code === 'island-never-with-registry-entry')).toEqual([])
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
it('flags multiple never-islands registered in the same hydrateIslands call', () => {
|
|
688
|
-
const code = `
|
|
689
|
-
import { island } from '@pyreon/server'
|
|
690
|
-
import { hydrateIslands } from '@pyreon/server/client'
|
|
691
|
-
export const A = island(() => import('./A'), { name: 'A', hydrate: 'never' })
|
|
692
|
-
export const B = island(() => import('./B'), { name: 'B', hydrate: 'never' })
|
|
693
|
-
hydrateIslands({
|
|
694
|
-
A: () => import('./A'),
|
|
695
|
-
B: () => import('./B'),
|
|
696
|
-
})
|
|
697
|
-
`
|
|
698
|
-
const diags = detectPyreonPatterns(code)
|
|
699
|
-
const hits = diags.filter((d) => d.code === 'island-never-with-registry-entry')
|
|
700
|
-
expect(hits).toHaveLength(2)
|
|
701
|
-
expect(hits.map((h) => h.message).join('|')).toContain('"A"')
|
|
702
|
-
expect(hits.map((h) => h.message).join('|')).toContain('"B"')
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
it('handles string-literal property keys in hydrateIslands', () => {
|
|
706
|
-
const code = `
|
|
707
|
-
import { island } from '@pyreon/server'
|
|
708
|
-
import { hydrateIslands } from '@pyreon/server/client'
|
|
709
|
-
export const X = island(() => import('./X'), { name: 'X', hydrate: 'never' })
|
|
710
|
-
hydrateIslands({
|
|
711
|
-
'X': () => import('./X'),
|
|
712
|
-
})
|
|
713
|
-
`
|
|
714
|
-
const diags = detectPyreonPatterns(code)
|
|
715
|
-
expect(
|
|
716
|
-
diags.filter((d) => d.code === 'island-never-with-registry-entry'),
|
|
717
|
-
).toHaveLength(1)
|
|
718
|
-
})
|
|
719
|
-
|
|
720
|
-
it('does NOT flag non-string `hydrate` values (variable indirection)', () => {
|
|
721
|
-
const code = `
|
|
722
|
-
import { island } from '@pyreon/server'
|
|
723
|
-
import { hydrateIslands } from '@pyreon/server/client'
|
|
724
|
-
const STRATEGY = 'never'
|
|
725
|
-
export const X = island(() => import('./X'), { name: 'X', hydrate: STRATEGY })
|
|
726
|
-
hydrateIslands({
|
|
727
|
-
X: () => import('./X'),
|
|
728
|
-
})
|
|
729
|
-
`
|
|
730
|
-
// The detector intentionally only recognizes string-literal hydrate
|
|
731
|
-
// values — variable indirection takes us past the static-detection
|
|
732
|
-
// surface into pyreon doctor --check-islands territory.
|
|
733
|
-
const diags = detectPyreonPatterns(code)
|
|
734
|
-
expect(diags.filter((d) => d.code === 'island-never-with-registry-entry')).toEqual([])
|
|
735
|
-
})
|
|
736
|
-
|
|
737
|
-
it('reports `fixable: false` (no auto-fix; manual edit required)', () => {
|
|
738
|
-
const code = `
|
|
739
|
-
import { island } from '@pyreon/server'
|
|
740
|
-
import { hydrateIslands } from '@pyreon/server/client'
|
|
741
|
-
export const X = island(() => import('./X'), { name: 'X', hydrate: 'never' })
|
|
742
|
-
hydrateIslands({
|
|
743
|
-
X: () => import('./X'),
|
|
744
|
-
})
|
|
745
|
-
`
|
|
746
|
-
const diags = detectPyreonPatterns(code)
|
|
747
|
-
const hit = diags.find((d) => d.code === 'island-never-with-registry-entry')
|
|
748
|
-
expect(hit).toBeDefined()
|
|
749
|
-
expect(hit!.fixable).toBe(false)
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
it('hasPyreonPatterns regex pre-filter recognizes the never-strategy form', () => {
|
|
753
|
-
expect(
|
|
754
|
-
hasPyreonPatterns(`island(() => import('./X'), { name: 'X', hydrate: 'never' })`),
|
|
755
|
-
).toBe(true)
|
|
756
|
-
})
|
|
757
|
-
})
|
|
758
|
-
|
|
759
|
-
describe('query-options-as-function', () => {
|
|
760
|
-
it('flags useQuery with an object-literal first arg', () => {
|
|
761
|
-
const code = `const q = useQuery({ queryKey: ['user', id()], queryFn: fetchUser })`
|
|
762
|
-
const diags = detectPyreonPatterns(code)
|
|
763
|
-
const d = diags.find((x) => x.code === 'query-options-as-function')
|
|
764
|
-
expect(d).toBeDefined()
|
|
765
|
-
expect(d!.fixable).toBe(false)
|
|
766
|
-
expect(d!.message).toContain('FUNCTION')
|
|
767
|
-
expect(d!.suggested).toBe(
|
|
768
|
-
"useQuery(() => ({ queryKey: ['user', id()], queryFn: fetchUser }))",
|
|
769
|
-
)
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
it('flags useInfiniteQuery / useQueries / useSuspenseQuery too', () => {
|
|
773
|
-
for (const hook of [
|
|
774
|
-
'useInfiniteQuery',
|
|
775
|
-
'useQueries',
|
|
776
|
-
'useSuspenseQuery',
|
|
777
|
-
]) {
|
|
778
|
-
const diags = detectPyreonPatterns(`const r = ${hook}({ queryKey: ['k'] })`)
|
|
779
|
-
expect(
|
|
780
|
-
diags.some((d) => d.code === 'query-options-as-function'),
|
|
781
|
-
).toBe(true)
|
|
782
|
-
}
|
|
783
|
-
})
|
|
784
|
-
|
|
785
|
-
it('does NOT flag the correct function form', () => {
|
|
786
|
-
const code = `const q = useQuery(() => ({ queryKey: ['user', id()], queryFn: fetchUser }))`
|
|
787
|
-
const diags = detectPyreonPatterns(code)
|
|
788
|
-
expect(
|
|
789
|
-
diags.some((d) => d.code === 'query-options-as-function'),
|
|
790
|
-
).toBe(false)
|
|
791
|
-
})
|
|
792
|
-
|
|
793
|
-
it('does NOT flag useMutation (options are a plain object by design)', () => {
|
|
794
|
-
const code = `const m = useMutation({ mutationFn: save, onSuccess })`
|
|
795
|
-
const diags = detectPyreonPatterns(code)
|
|
796
|
-
expect(
|
|
797
|
-
diags.some((d) => d.code === 'query-options-as-function'),
|
|
798
|
-
).toBe(false)
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
it('does NOT flag an identifier / call options arg (unprovable)', () => {
|
|
802
|
-
const code = `const a = useQuery(opts); const b = useQuery(makeOpts())`
|
|
803
|
-
const diags = detectPyreonPatterns(code)
|
|
804
|
-
expect(
|
|
805
|
-
diags.some((d) => d.code === 'query-options-as-function'),
|
|
806
|
-
).toBe(false)
|
|
807
|
-
})
|
|
808
|
-
|
|
809
|
-
it('hasPyreonPatterns pre-filter recognizes the object-literal form', () => {
|
|
810
|
-
expect(hasPyreonPatterns(`useQuery({ queryKey: ['k'] })`)).toBe(true)
|
|
811
|
-
expect(hasPyreonPatterns(`useQuery(() => ({ queryKey: ['k'] }))`)).toBe(
|
|
812
|
-
false,
|
|
813
|
-
)
|
|
814
|
-
})
|
|
815
|
-
})
|
|
816
|
-
})
|