@pyreon/compiler 0.18.0 → 0.20.0
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +2081 -1262
- package/lib/types/index.d.ts +310 -125
- package/package.json +14 -12
- package/src/defer-inline.ts +397 -157
- package/src/index.ts +14 -2
- package/src/jsx.ts +784 -19
- package/src/load-native.ts +1 -0
- package/src/manifest.ts +280 -0
- package/src/pyreon-intercept.ts +164 -0
- package/src/react-intercept.ts +59 -0
- package/src/reactivity-lens.ts +190 -0
- package/src/tests/backend-parity-r7-r9.test.ts +91 -0
- package/src/tests/backend-prop-derived-callback-divergence.test.ts +74 -0
- package/src/tests/collapse-bail-census.test.ts +245 -0
- package/src/tests/collapse-key-source-hygiene.test.ts +88 -0
- package/src/tests/defer-inline.test.ts +209 -21
- package/src/tests/detector-tag-consistency.test.ts +2 -0
- package/src/tests/element-valued-const-child.test.ts +61 -0
- package/src/tests/falsy-child-characterization.test.ts +48 -0
- package/src/tests/malformed-input-resilience.test.ts +50 -0
- package/src/tests/manifest-snapshot.test.ts +55 -0
- package/src/tests/native-equivalence.test.ts +104 -3
- package/src/tests/partial-collapse-detector.test.ts +121 -0
- package/src/tests/partial-collapse-emit.test.ts +104 -0
- package/src/tests/partial-collapse-robustness.test.ts +53 -0
- package/src/tests/prop-derived-shadow.test.ts +96 -0
- package/src/tests/pure-call-reactive-args.test.ts +50 -0
- package/src/tests/pyreon-intercept.test.ts +189 -0
- package/src/tests/r13-callback-stmt-equivalence.test.ts +58 -0
- package/src/tests/r14-ssr-mode-parity.test.ts +51 -0
- package/src/tests/r15-elemconst-propderived.test.ts +47 -0
- package/src/tests/r19-defer-inline-robust.test.ts +54 -0
- package/src/tests/r20-backend-equivalence-sweep.test.ts +50 -0
- package/src/tests/react-intercept.test.ts +50 -2
- package/src/tests/reactivity-lens.test.ts +170 -0
- package/src/tests/rocketstyle-collapse.test.ts +208 -0
- package/src/tests/signal-autocall-shadow.test.ts +86 -0
- package/src/tests/sourcemap-fidelity.test.ts +77 -0
- package/src/tests/static-text-baking.test.ts +64 -0
- package/src/tests/transform-state-isolation.test.ts +49 -0
|
@@ -84,6 +84,137 @@ describe('detectPyreonPatterns', () => {
|
|
|
84
84
|
})
|
|
85
85
|
})
|
|
86
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
|
+
|
|
87
218
|
describe('process-dev-gate', () => {
|
|
88
219
|
it('flags typeof process + NODE_ENV production gates', () => {
|
|
89
220
|
const code = `
|
|
@@ -624,4 +755,62 @@ describe('detectPyreonPatterns', () => {
|
|
|
624
755
|
).toBe(true)
|
|
625
756
|
})
|
|
626
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
|
+
})
|
|
627
816
|
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 13 (REAL JS↔Rust divergence, FIXED + bisect).
|
|
3
|
+
*
|
|
4
|
+
* R7 (#687) taught the native `collect_prop_derived_idents` to recurse into
|
|
5
|
+
* callback bodies — but its `collect_pd_in_stmt` only handled
|
|
6
|
+
* Expression/Return/VarDecl/If/Block, with `_ => {}` skipping
|
|
7
|
+
* For/While/DoWhile/Switch/Try/Labeled. So a prop-derived const used inside a
|
|
8
|
+
* callback whose body is one of those shapes still lost reactivity in the
|
|
9
|
+
* NATIVE backend (preferred in prod) while the JS backend inlined it — the
|
|
10
|
+
* exact R7 reactivity-loss class, narrower shapes (e.g. `try { return <li
|
|
11
|
+
* class={c}/> } catch …` in a render callback — plausible defensive render).
|
|
12
|
+
*
|
|
13
|
+
* Fix: native/src/lib.rs `collect_pd_in_stmt` now also handles
|
|
14
|
+
* For/ForIn/ForOf/While/DoWhile/Switch/Try/Labeled, with the SAME
|
|
15
|
+
* `pd_minus`/`collect_bind_pattern_names` shadow-filter discipline as the
|
|
16
|
+
* Block/If arms (so loop/catch-param bindings shadow correctly and the
|
|
17
|
+
* over-substitution clobber is NOT re-introduced — verified by the
|
|
18
|
+
* catch-param spec).
|
|
19
|
+
*
|
|
20
|
+
* Bisect: replace the new arms with `_ => {}` + `bun scripts/build-native.ts`
|
|
21
|
+
* → these specs fail (Rust emits `class={c}`, JS `class={(p.x+'-b')}`).
|
|
22
|
+
* Restore + rebuild → all pass. The 180 native-equivalence tests + full
|
|
23
|
+
* suite remain green (no regression).
|
|
24
|
+
*/
|
|
25
|
+
import { describe, expect, it } from 'vitest'
|
|
26
|
+
import { transformJSX, transformJSX_JS } from '../jsx'
|
|
27
|
+
|
|
28
|
+
const j = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
|
|
29
|
+
const r = (c: string): string => transformJSX(c, 'c.tsx').code ?? ''
|
|
30
|
+
|
|
31
|
+
const CASES: Array<[string, string]> = [
|
|
32
|
+
['while', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { while(i){ return <li class={c}/> } })}</ul> }`],
|
|
33
|
+
['switch', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { switch(i){ default: return <li class={c}/> } })}</ul> }`],
|
|
34
|
+
['labeled', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { lbl: { return <li class={c}/> } })}</ul> }`],
|
|
35
|
+
['try-catch', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { try { return <li class={c}/> } catch { return null } })}</ul> }`],
|
|
36
|
+
['for', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { for(let k=0;k<i;k++){ return <li class={c}/> } })}</ul> }`],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
describe('Round 13 — prop-derived inlining inside callback statement shapes is JS≡Rust', () => {
|
|
40
|
+
for (const [name, src] of CASES) {
|
|
41
|
+
it(`${name}: native backend matches JS (inlines the prop-derived const)`, () => {
|
|
42
|
+
expect(r(src)).toBe(j(src))
|
|
43
|
+
expect(r(src)).toContain("class={(p.x+'-b')}")
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it('catch-param shadowing is NOT clobbered (filter discipline, both backends)', () => {
|
|
48
|
+
const src = `function C(p){ const e=p.x+'-b'; return <ul>{p.i.map(i => { try {} catch (e) { return <li class={e}/> } })}</ul> }`
|
|
49
|
+
expect(r(src)).toBe(j(src))
|
|
50
|
+
expect(r(src)).not.toContain("class={(p.x+'-b')}") // `e` is the catch param
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('nested const inside a while body shadowing the prop-derived is NOT clobbered (no R2 regression)', () => {
|
|
54
|
+
const src = `function C(p){ const c=p.x; return <ul>{p.i.map(i => { while(i){ const c=2; return <li>{c}</li> } })}</ul> }`
|
|
55
|
+
expect(r(src)).toBe(j(src))
|
|
56
|
+
expect(r(src)).not.toContain('{(p.x)}') // inner `const c=2` shadows — must stay `{c}`
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 14 (SSR-mode robustness; no bug found).
|
|
3
|
+
*
|
|
4
|
+
* Rounds 1–13 were all client-mode. SSR mode (`{ ssr: true }`) disables
|
|
5
|
+
* template emission (`if (ssr) return false`) and leaves JSX as
|
|
6
|
+
* accessor-wrapped expressions for the SSR renderer. Probed 8 adversarial
|
|
7
|
+
* shapes — all emit parseable code, and the correctness-critical contracts
|
|
8
|
+
* hold in SSR exactly as in client mode: prop-derived consts still inline,
|
|
9
|
+
* signals still auto-call, and the R11 shadow fix still suppresses a
|
|
10
|
+
* destructured callback param. This locks SSR-mode parity for the bug fixes
|
|
11
|
+
* so a future SSR-path change can't silently regress them.
|
|
12
|
+
*/
|
|
13
|
+
import { parseSync } from 'oxc-parser'
|
|
14
|
+
import { describe, expect, it } from 'vitest'
|
|
15
|
+
import { transformJSX_JS } from '../jsx'
|
|
16
|
+
|
|
17
|
+
const ssr = (c: string): string => transformJSX_JS(c, 'c.tsx', { ssr: true }).code ?? ''
|
|
18
|
+
const parses = (o: string): boolean => {
|
|
19
|
+
try {
|
|
20
|
+
return (parseSync('o.tsx', o).errors?.length ?? 0) === 0
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('Round 14 — SSR-mode codegen parity', () => {
|
|
27
|
+
it('SSR never throws / always emits parseable code (8 adversarial shapes)', () => {
|
|
28
|
+
const shapes = [
|
|
29
|
+
`function C(p){ return <div class={p.c}>{p.t}</div> }`,
|
|
30
|
+
`function C(p){ return <div {...p}>{p.k}</div> }`,
|
|
31
|
+
`function C(p){ return <>{p.a}<span>{p.b}</span></> }`,
|
|
32
|
+
`function C(){ const h=<h1>T</h1>; return <div>{h}<p>x</p></div> }`,
|
|
33
|
+
`function C(p){ return <section><h1>{p.t}</h1><p>{p.b}</p></section> }`,
|
|
34
|
+
]
|
|
35
|
+
for (const s of shapes) expect(parses(ssr(s)), s).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('prop-derived const still inlines in SSR (R2/R7 parity)', () => {
|
|
39
|
+
expect(ssr(`function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i=><li class={c}>{i}</li>)}</ul> }`))
|
|
40
|
+
.toContain("class={(p.x+'-b')}")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('signal still auto-calls in SSR', () => {
|
|
44
|
+
expect(ssr(`function C(){ const s=signal(0); return <div>{s}</div> }`)).toContain('s()')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('R11 shadow fix applies in SSR (destructured param NOT auto-called)', () => {
|
|
48
|
+
expect(ssr(`function C(){ const x=signal(0); return <ul>{[{x:1}].map(({x})=><li>{x}</li>)}</ul> }`))
|
|
49
|
+
.not.toContain('{x()}')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 15 (element-const that references a prop-derived
|
|
3
|
+
* var) — consolidated, deterministic JS-backend characterization.
|
|
4
|
+
*
|
|
5
|
+
* Original finding: `const cls = props.x + '-b'; const el = <i class={cls}/>;`
|
|
6
|
+
* used as a bare `{el}` child sat at the intersection of R2 (prop-derived
|
|
7
|
+
* inlining) and R9 (element-const → `_mountSlot`). The JS backend substitutes
|
|
8
|
+
* the element-const's whole initializer into the mount call with the
|
|
9
|
+
* prop-derived part inlined reactively:
|
|
10
|
+
* `_mountSlot((<i class={(props.x + '-b')}/>), …)`
|
|
11
|
+
*
|
|
12
|
+
* It was previously tracked by two duplicate, environment-fragile `it.fails`
|
|
13
|
+
* JS↔Rust divergence locks. The R13 gate/collector fix
|
|
14
|
+
* (`accesses_props` now recurses arrow/JSX → the element-const's prop-derived
|
|
15
|
+
* ref is collected) realigned the native backend with JS — so the locks
|
|
16
|
+
* correctly auto-flipped and were removed (lock→resolved, same lifecycle as
|
|
17
|
+
* R7/R11/R13). JS↔Rust byte-equivalence is now gated generally by the R20
|
|
18
|
+
* sweep + R13's own contract; this file keeps only the DETERMINISTIC,
|
|
19
|
+
* native-independent JS-backend assertions (no `transformJSX` native call →
|
|
20
|
+
* no build-artifact fragility, stable on every runner).
|
|
21
|
+
*/
|
|
22
|
+
import { describe, expect, it } from 'vitest'
|
|
23
|
+
import { transformJSX_JS } from '../jsx'
|
|
24
|
+
|
|
25
|
+
const emit = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
|
|
26
|
+
|
|
27
|
+
const PD_ELEM = `function C(p){ const cls=p.x+'-b'; const el=<i class={cls}/>; return <div>{el}<span class={cls}/></div> }`
|
|
28
|
+
const SIMPLE = `function C(){ const h=<h1>T</h1>; return <div>{h}<p>x</p></div> }`
|
|
29
|
+
const REUSED = `function C(){ const ic=<svg/>; return <div>{ic}<span>{ic}</span></div> }`
|
|
30
|
+
|
|
31
|
+
describe('Round 15 — element-const × prop-derived (JS backend characterization)', () => {
|
|
32
|
+
it('JS inlines a prop-derived-referencing element-const into _mountSlot with a reactive class', () => {
|
|
33
|
+
const out = emit(PD_ELEM)
|
|
34
|
+
expect(out).toContain("_mountSlot((<i class={(p.x+'-b')}/>)")
|
|
35
|
+
// the prop-derived class is the reactive form, not the frozen const ref
|
|
36
|
+
expect(out).not.toMatch(/_mountSlot\(\s*el\b/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('a simple element-const child routes through _mountSlot (R9 baseline)', () => {
|
|
40
|
+
expect(emit(SIMPLE)).toContain('_mountSlot(h')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('an element-const reused at two sites routes both through _mountSlot', () => {
|
|
44
|
+
const out = emit(REUSED)
|
|
45
|
+
expect(out).toMatch(/_mountSlot\(\s*ic\b/)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 19 (defer-inline robustness; no bug found).
|
|
3
|
+
*
|
|
4
|
+
* `transformDeferInline` (a separate compiler entry, unprobed in rounds
|
|
5
|
+
* 1–18) rewrites `<Defer when={…}><X/></Defer>` to inline its children.
|
|
6
|
+
* Probed 10 valid shapes (no-children, multi-child, expr/text child, nested
|
|
7
|
+
* Defer, self-closing, spread child, comment child, passthrough) + a
|
|
8
|
+
* malformed input — all valid cases emit parseable code, and malformed input
|
|
9
|
+
* is passed through unchanged without throwing (same resilience contract as
|
|
10
|
+
* Round 10). This locks the surface.
|
|
11
|
+
*/
|
|
12
|
+
import { parseSync } from 'oxc-parser'
|
|
13
|
+
import { describe, expect, it } from 'vitest'
|
|
14
|
+
import { transformDeferInline } from '../defer-inline'
|
|
15
|
+
|
|
16
|
+
const run = (c: string): string => {
|
|
17
|
+
const r = transformDeferInline(c, 'c.tsx') as { code?: string }
|
|
18
|
+
return r?.code ?? ''
|
|
19
|
+
}
|
|
20
|
+
const parses = (o: string): boolean => {
|
|
21
|
+
try {
|
|
22
|
+
return (parseSync('o.tsx', o).errors?.length ?? 0) === 0
|
|
23
|
+
} catch {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const VALID: Array<[string, string]> = [
|
|
29
|
+
['basic', `import { Modal } from './M'\nfunction C(){ const o=signal(0); return <Defer when={o()}><Modal title="hi"/></Defer> }`],
|
|
30
|
+
['passthrough', `function C(){ return <div>{x}</div> }`],
|
|
31
|
+
['no-children', `function C(){ return <Defer when={a}></Defer> }`],
|
|
32
|
+
['multi-child', `import {A,B} from './x'\nfunction C(){ return <Defer when={c}><A/><B/></Defer> }`],
|
|
33
|
+
['expr-child', `function C(){ return <Defer when={c}>{val}</Defer> }`],
|
|
34
|
+
['nested-defer', `import {A} from './x'\nfunction C(){ return <Defer when={a}><Defer when={b}><A/></Defer></Defer> }`],
|
|
35
|
+
['text-child', `function C(){ return <Defer when={c}>plain text</Defer> }`],
|
|
36
|
+
['self-closing', `function C(){ return <Defer when={c}/> }`],
|
|
37
|
+
['spread-child', `import {A} from './x'\nfunction C(p){ return <Defer when={c}><A {...p}/></Defer> }`],
|
|
38
|
+
['comment-child', `function C(){ return <Defer when={c}>{/* x */}</Defer> }`],
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
describe('Round 19 — transformDeferInline robustness', () => {
|
|
42
|
+
for (const [name, src] of VALID) {
|
|
43
|
+
it(`emits parseable code: ${name}`, () => {
|
|
44
|
+
let out = ''
|
|
45
|
+
expect(() => {
|
|
46
|
+
out = run(src)
|
|
47
|
+
}).not.toThrow()
|
|
48
|
+
expect(parses(out)).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
it('malformed input is passed through without throwing (resilience)', () => {
|
|
52
|
+
expect(() => run(`function C(){ return <Defer when={</Defer> }`)).not.toThrow()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 20 (JS↔Rust equivalence sweep, R11–R19 corpus).
|
|
3
|
+
*
|
|
4
|
+
* The dual backends MUST emit byte-identical output. This sweep locks the
|
|
5
|
+
* adversarial corpus surfaced in rounds 11–19 where BOTH backends agree, so a
|
|
6
|
+
* future one-backend change that introduces a divergence (the recurring
|
|
7
|
+
* R7/R13/R15 one-backend-change failure mode) is caught immediately. R11's
|
|
8
|
+
* and R13's fixes CONVERGED the backends (R11 brought JS onto the
|
|
9
|
+
* already-correct Rust; R13's gate/collector fix realigned native with JS,
|
|
10
|
+
* which also resolved the R15 element-const×prop-derived divergence) — those
|
|
11
|
+
* shapes must now stay identical, asserted here. The earlier self-
|
|
12
|
+
* discriminating `it.fails` divergence locks auto-flipped on resolution and
|
|
13
|
+
* were removed; R15's residual is a deterministic JS-backend
|
|
14
|
+
* characterization (r15-elemconst-propderived.test.ts).
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
import { transformJSX, transformJSX_JS } from '../jsx'
|
|
18
|
+
|
|
19
|
+
const EQUIVALENT: Array<[string, string]> = [
|
|
20
|
+
// R11 — signal-auto-call shadowing (JS converged to Rust)
|
|
21
|
+
['r11-destructured-param', `function C(){ const x = signal(0); return <ul>{[{x:1}].map(({x}) => <li>{x}</li>)}</ul> }`],
|
|
22
|
+
['r11-filter-obj-param', `function C(){ const id = signal(0); return <ul>{[{id:1}].filter(({id}) => id > 0).map(r => <li>{r}</li>)}</ul> }`],
|
|
23
|
+
['r11-plain-param-shadow', `function C(){ const s = signal(0); return <ul>{[1].map(s => <li>{s}</li>)}</ul> }`],
|
|
24
|
+
['r11-direct-signal', `function C(){ const s = signal(0); return <div>{s}</div> }`],
|
|
25
|
+
['r11-signal-shadow-sibling', `function C(){ const s = signal(0); return <div>{[1].map(s => <i>{s}</i>)}<b>{s}</b></div> }`],
|
|
26
|
+
// R16 — knownSignals (cross-module signal) path
|
|
27
|
+
['r16-knownsignal-shadow', `function C(){ return <ul>{[{count:1}].map(({count}) => <li>{count}</li>)}</ul> }`],
|
|
28
|
+
// R15 — element-const interactions that DO converge
|
|
29
|
+
['r15-simple-elem-const', `function C(){ const h=<h1>T</h1>; return <div>{h}<p>x</p></div> }`],
|
|
30
|
+
['r15-elem-const-reused', `function C(){ const ic=<svg/>; return <div>{ic}<span>{ic}</span></div> }`],
|
|
31
|
+
// R17 — spread depth
|
|
32
|
+
['r17-root-spread-reactive', `function C(p){ const s=signal(0); return <div {...p} onClick={()=>s.set(1)}>{s}</div> }`],
|
|
33
|
+
['r17-component-spread-pd', `function C(p){ const c=p.x+'-b'; return <Comp {...p} class={c}/> }`],
|
|
34
|
+
['r17-double-spread', `function C(p){ return <div {...p.a} {...p.b}>{p.c}</div> }`],
|
|
35
|
+
// R18 — hoisting × elementVars
|
|
36
|
+
['r18-hoist-vs-elemconst', `function C(){ const e=<b>x</b>; return <div>{<i/>}{e}</div> }`],
|
|
37
|
+
['r18-elemconst-reused-3x', `function C(){ const e=<hr/>; return <div>{e}{e}{e}</div> }`],
|
|
38
|
+
// R13 — the convergent control (return-body callback)
|
|
39
|
+
['r13-return-cb', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { return <li class={c}>{i}</li> })}</ul> }`],
|
|
40
|
+
// general
|
|
41
|
+
['mixed', `function C(p){ const a=p.x; return <section><h1>{a}</h1>{p.i.map(i=><p key={i.id}>{i.t}</p>)}</section> }`],
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
describe('Round 20 — JS↔Rust byte-equivalence (R11–R19 corpus)', () => {
|
|
45
|
+
for (const [name, code] of EQUIVALENT) {
|
|
46
|
+
it(`${name}: JS ≡ Rust`, () => {
|
|
47
|
+
expect(transformJSX(code, 'c.tsx').code).toBe(transformJSX_JS(code, 'c.tsx').code)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
})
|
|
@@ -333,8 +333,8 @@ describe('detectReactPatterns', () => {
|
|
|
333
333
|
expect(d!.fixable).toBe(true)
|
|
334
334
|
})
|
|
335
335
|
|
|
336
|
-
test('detects .value assignment on signal
|
|
337
|
-
const code = 'count.value = 5'
|
|
336
|
+
test('detects .value assignment on a declared signal', () => {
|
|
337
|
+
const code = 'const count = signal(0)\ncount.value = 5'
|
|
338
338
|
const diags = detectReactPatterns(code)
|
|
339
339
|
const d = diags.find((d) => d.code === 'dot-value-signal')
|
|
340
340
|
expect(d).toBeDefined()
|
|
@@ -343,6 +343,54 @@ describe('detectReactPatterns', () => {
|
|
|
343
343
|
expect(d!.fixable).toBe(false)
|
|
344
344
|
})
|
|
345
345
|
|
|
346
|
+
// ─── dot-value-signal precision (FP-free): only flag tracked signals ──────
|
|
347
|
+
|
|
348
|
+
test('flags .value write on signal(), computed(), useSignal(), createSignal()', () => {
|
|
349
|
+
for (const factory of ['signal', 'computed', 'useSignal', 'createSignal']) {
|
|
350
|
+
const code = `const s = ${factory}(0)\ns.value = 1`
|
|
351
|
+
const diags = detectReactPatterns(code)
|
|
352
|
+
const d = diags.find((d) => d.code === 'dot-value-signal')
|
|
353
|
+
expect(d, `expected dot-value-signal for ${factory}`).toBeDefined()
|
|
354
|
+
expect(d!.suggested).toContain('s.set(1)')
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
test('does NOT flag input.value = "" (DOM element, not a signal)', () => {
|
|
359
|
+
const code = `const input = document.querySelector("input")\ninput.value = ""`
|
|
360
|
+
const diags = detectReactPatterns(code)
|
|
361
|
+
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test('does NOT flag cell.value = x (data object, not a signal)', () => {
|
|
365
|
+
const code = `const cell = sheet.getCell(1, 1)\ncell.value = "hello"`
|
|
366
|
+
const diags = detectReactPatterns(code)
|
|
367
|
+
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('does NOT flag o.value = y (loop option object, not a signal)', () => {
|
|
371
|
+
const code = `for (const o of options) { o.value = y }`
|
|
372
|
+
const diags = detectReactPatterns(code)
|
|
373
|
+
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test('does NOT flag ref.current.value = z (ref pattern, not a signal)', () => {
|
|
377
|
+
const code = `const ref = useRef(null)\nref.current.value = 42`
|
|
378
|
+
const diags = detectReactPatterns(code)
|
|
379
|
+
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('does NOT flag bare X.value = n with no signal declaration', () => {
|
|
383
|
+
const code = 'count.value = 5'
|
|
384
|
+
const diags = detectReactPatterns(code)
|
|
385
|
+
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('does NOT flag .value write on a let/var (mutable, unreliable)', () => {
|
|
389
|
+
const code = `let maybe = signal(0)\nmaybe = 5\nmaybe.value = 1`
|
|
390
|
+
const diags = detectReactPatterns(code)
|
|
391
|
+
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
392
|
+
})
|
|
393
|
+
|
|
346
394
|
test('detects .map() in JSX expression', () => {
|
|
347
395
|
const code = 'const el = <ul>{items.map(item => <li>{item}</li>)}</ul>'
|
|
348
396
|
const diags = detectReactPatterns(code)
|