@barefootjs/jsx 0.18.5 → 0.19.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/dist/adapters/dangerous-inner-html.d.ts +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +8 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1002 -284
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +23 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts +0 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +21 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/client-js-generation.test.ts +40 -0
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/props-destructuring.test.ts +61 -8
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +9 -0
- package/src/analyzer.ts +36 -22
- package/src/expression-parser.ts +33 -0
- package/src/index.ts +20 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +53 -0
- package/src/ir-to-client-js/html-template.ts +235 -2
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/phases/props-extraction.ts +5 -9
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +21 -0
- package/src/jsx-to-ir.ts +230 -39
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { analyzeComponent } from '../analyzer'
|
|
3
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
4
|
+
import { collectLoopBoundNames } from '../adapters/loop-bound-names'
|
|
5
|
+
import type { ComponentIR } from '../types'
|
|
6
|
+
|
|
7
|
+
// #2212 (Fable review): `collectLoopBoundNames` finds every name a loop
|
|
8
|
+
// callback binds as its item/index parameter, so each adapter's
|
|
9
|
+
// `collectStringValueNames` can exclude it from string-concat detection —
|
|
10
|
+
// a loop param shadowing an outer string-typed binding of the same name
|
|
11
|
+
// must never be misdetected as string-typed.
|
|
12
|
+
describe('collectLoopBoundNames (#2212)', () => {
|
|
13
|
+
// `collectLoopBoundNames` only reads `.root`; the rest of `ComponentIR`
|
|
14
|
+
// is irrelevant to this walk, so a minimal stand-in is fine here.
|
|
15
|
+
function ir(source: string): ComponentIR {
|
|
16
|
+
const ctx = analyzeComponent(source, 'Test.tsx')
|
|
17
|
+
const root = jsxToIR(ctx)
|
|
18
|
+
if (!root) throw new Error('expected IR')
|
|
19
|
+
return { version: '0.1', metadata: {} as ComponentIR['metadata'], root, errors: [] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('a component with no loops has no bound names', () => {
|
|
23
|
+
const component = ir(`
|
|
24
|
+
function Test() { return <div>hi</div> }
|
|
25
|
+
export { Test }
|
|
26
|
+
`)
|
|
27
|
+
expect(collectLoopBoundNames(component).size).toBe(0)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('a simple loop param is bound', () => {
|
|
31
|
+
const component = ir(`
|
|
32
|
+
function Test({ items }: { items: string[] }) {
|
|
33
|
+
return <ul>{items.map(name => <li key={name}>{name}</li>)}</ul>
|
|
34
|
+
}
|
|
35
|
+
export { Test }
|
|
36
|
+
`)
|
|
37
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['name']))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('both item and index params are bound', () => {
|
|
41
|
+
const component = ir(`
|
|
42
|
+
function Test({ items }: { items: string[] }) {
|
|
43
|
+
return <ul>{items.map((name, idx) => <li key={idx}>{name}</li>)}</ul>
|
|
44
|
+
}
|
|
45
|
+
export { Test }
|
|
46
|
+
`)
|
|
47
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['name', 'idx']))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('nested loops contribute all their params', () => {
|
|
51
|
+
const component = ir(`
|
|
52
|
+
function Test({ groups }: { groups: { label: string; items: string[] }[] }) {
|
|
53
|
+
return <div>{groups.map(group => <ul key={group.label}>{group.items.map(item => <li key={item}>{item}</li>)}</ul>)}</div>
|
|
54
|
+
}
|
|
55
|
+
export { Test }
|
|
56
|
+
`)
|
|
57
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['group', 'item']))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('a loop body wrapping a child component still contributes its param', () => {
|
|
61
|
+
const component = ir(`
|
|
62
|
+
import { Row } from './row'
|
|
63
|
+
function Test({ items }: { items: string[] }) {
|
|
64
|
+
return <ul>{items.map(item => <Row key={item} label={item} />)}</ul>
|
|
65
|
+
}
|
|
66
|
+
export { Test }
|
|
67
|
+
`)
|
|
68
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['item']))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Fable re-review (#2212): `param` holds the raw pattern text for a
|
|
72
|
+
// destructured callback (`"{ name }"`), not the bound name itself — the
|
|
73
|
+
// real bound names live in `paramBindings`. Adapters that lower a
|
|
74
|
+
// destructure to a `{% set name = __bf_item.name %}`-style local (#2087)
|
|
75
|
+
// leave `name` reachable as a bare identifier in the loop body, so it
|
|
76
|
+
// needs the same exclusion as a plain loop param. `node.param`'s raw
|
|
77
|
+
// pattern text still ends up in the set too (added unconditionally,
|
|
78
|
+
// before this arm existed) — harmless noise, since it can never collide
|
|
79
|
+
// with a real identifier name.
|
|
80
|
+
test('a destructured loop param contributes its extracted binding names', () => {
|
|
81
|
+
const component = ir(`
|
|
82
|
+
function Test({ rows }: { rows: { name: string }[] }) {
|
|
83
|
+
return <ul>{rows.map(({ name }) => <li key={name}>{name}</li>)}</ul>
|
|
84
|
+
}
|
|
85
|
+
export { Test }
|
|
86
|
+
`)
|
|
87
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['{ name }', 'name']))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Fable re-review (#2212): a `.filter(pred).map(cb)` chain's filter
|
|
91
|
+
// predicate is emitted through the same binary/string-name machinery
|
|
92
|
+
// using its OWN param, which may differ from the map callback's `param`.
|
|
93
|
+
test('a filter().map() chain contributes both the filter predicate param and the map param', () => {
|
|
94
|
+
const component = ir(`
|
|
95
|
+
function Test({ values }: { values: number[] }) {
|
|
96
|
+
return <ul>{values.filter(n => n > 3).map(v => <li key={v}>{v}</li>)}</ul>
|
|
97
|
+
}
|
|
98
|
+
export { Test }
|
|
99
|
+
`)
|
|
100
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['n', 'v']))
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -92,6 +92,7 @@ describe('buildLoopPlan decision tree (#1253)', () => {
|
|
|
92
92
|
kind: 'nested',
|
|
93
93
|
array: 'item.subs',
|
|
94
94
|
param: 'sub',
|
|
95
|
+
index: null,
|
|
95
96
|
key: null,
|
|
96
97
|
markerId: 'inner-m1',
|
|
97
98
|
template: '<span></span>',
|
|
@@ -161,6 +162,7 @@ describe('buildLoopPlan decision tree (#1253)', () => {
|
|
|
161
162
|
kind: 'nested',
|
|
162
163
|
array: 'item.subs',
|
|
163
164
|
param: 'sub',
|
|
165
|
+
index: null,
|
|
164
166
|
key: null,
|
|
165
167
|
markerId: 'inner-m1',
|
|
166
168
|
template: '<span></span>',
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { analyzeComponent } from '../analyzer'
|
|
3
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
4
|
+
import { asCallbackMethodCall, isSupported } from '../expression-parser'
|
|
5
|
+
import type { IRExpression } from '../types'
|
|
6
|
+
|
|
7
|
+
// #2206: a bare-identifier callback passed to a value-position higher-order
|
|
8
|
+
// array method (`tags.map(format)`) resolves one hop to its declaration
|
|
9
|
+
// (`resolveCallbackMethodFunctionReferences` in jsx-to-ir.ts), reusing the
|
|
10
|
+
// SAME `findLocalConst` / `findLocalFunction` scope machinery #2090
|
|
11
|
+
// established for `.sort(cb)` — but as a post-parse pass over the whole
|
|
12
|
+
// `ParsedExpr` tree, since the value-returning `.map(cb).join(...)` form is
|
|
13
|
+
// recognized generically downstream of parsing (`asCallbackMethodCall`), not
|
|
14
|
+
// during it.
|
|
15
|
+
describe('function-reference .map(cb) callback (#2206)', () => {
|
|
16
|
+
function textExpr(ir: ReturnType<typeof jsxToIR>): IRExpression {
|
|
17
|
+
if (!ir || ir.type !== 'element') throw new Error('expected a root element')
|
|
18
|
+
const expr = ir.children.find(c => c.type === 'expression')
|
|
19
|
+
if (!expr || expr.type !== 'expression') throw new Error('expected an expression child')
|
|
20
|
+
return expr
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('const arrow reference resolves like an inline arrow', () => {
|
|
24
|
+
const source = `
|
|
25
|
+
const format = (t: string) => '#' + t
|
|
26
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
27
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
28
|
+
}
|
|
29
|
+
export { TagLine }
|
|
30
|
+
`
|
|
31
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
32
|
+
const ir = jsxToIR(ctx)
|
|
33
|
+
const expr = textExpr(ir)
|
|
34
|
+
|
|
35
|
+
expect(expr.parsed).toBeDefined()
|
|
36
|
+
// .join(' ') is the outer call; its object is the resolved .map(...) call.
|
|
37
|
+
const joinCall = expr.parsed!
|
|
38
|
+
expect(joinCall.kind).toBe('array-method')
|
|
39
|
+
if (joinCall.kind !== 'array-method') return
|
|
40
|
+
const mapCall = asCallbackMethodCall(joinCall.object)
|
|
41
|
+
expect(mapCall).not.toBeNull()
|
|
42
|
+
expect(mapCall!.method).toBe('map')
|
|
43
|
+
expect(mapCall!.arrow.kind).toBe('arrow')
|
|
44
|
+
expect(mapCall!.arrow.params).toEqual(['t'])
|
|
45
|
+
// Resolved body is identical to what an inline `t => '#' + t` would parse to.
|
|
46
|
+
expect(isSupported(joinCall).supported).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('function-declaration reference resolves too', () => {
|
|
50
|
+
const source = `
|
|
51
|
+
function format(t: string) { return '#' + t }
|
|
52
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
53
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
54
|
+
}
|
|
55
|
+
export { TagLine }
|
|
56
|
+
`
|
|
57
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
58
|
+
const ir = jsxToIR(ctx)
|
|
59
|
+
const expr = textExpr(ir)
|
|
60
|
+
|
|
61
|
+
expect(expr.parsed).toBeDefined()
|
|
62
|
+
const joinCall = expr.parsed!
|
|
63
|
+
expect(joinCall.kind).toBe('array-method')
|
|
64
|
+
if (joinCall.kind !== 'array-method') return
|
|
65
|
+
const mapCall = asCallbackMethodCall(joinCall.object)
|
|
66
|
+
expect(mapCall).not.toBeNull()
|
|
67
|
+
expect(mapCall!.arrow.params).toEqual(['t'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('component-scope const shadows a module-scope const of the same name', () => {
|
|
71
|
+
const source = `
|
|
72
|
+
const format = (t: string) => 'module:' + t
|
|
73
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
74
|
+
const format = (t: string) => 'local:' + t
|
|
75
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
76
|
+
}
|
|
77
|
+
export { TagLine }
|
|
78
|
+
`
|
|
79
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
80
|
+
const ir = jsxToIR(ctx)
|
|
81
|
+
const expr = textExpr(ir)
|
|
82
|
+
const joinCall = expr.parsed!
|
|
83
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
84
|
+
const mapCall = asCallbackMethodCall(joinCall.object)!
|
|
85
|
+
// The resolved body must be the component-scope binding ('local:'), not
|
|
86
|
+
// the module-scope one ('module:').
|
|
87
|
+
expect(JSON.stringify(mapCall.arrow.body)).toContain('local:')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('unresolvable reference (imported) stays refused with BF101', () => {
|
|
91
|
+
const source = `
|
|
92
|
+
import { format } from './format'
|
|
93
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
94
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
95
|
+
}
|
|
96
|
+
export { TagLine }
|
|
97
|
+
`
|
|
98
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
99
|
+
const ir = jsxToIR(ctx)
|
|
100
|
+
const expr = textExpr(ir)
|
|
101
|
+
const joinCall = expr.parsed!
|
|
102
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
103
|
+
// The identifier arg passes through untouched — asCallbackMethodCall
|
|
104
|
+
// still refuses it, so the adapter's UNSUPPORTED_METHODS gate fires.
|
|
105
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
106
|
+
expect(isSupported(joinCall).supported).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('non-function const (unresolvable) stays refused with BF101', () => {
|
|
110
|
+
const source = `
|
|
111
|
+
const format = 5
|
|
112
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
113
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
114
|
+
}
|
|
115
|
+
export { TagLine }
|
|
116
|
+
`
|
|
117
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
118
|
+
const ir = jsxToIR(ctx)
|
|
119
|
+
const expr = textExpr(ir)
|
|
120
|
+
const joinCall = expr.parsed!
|
|
121
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
122
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
123
|
+
expect(isSupported(joinCall).supported).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('/* @client */ still suppresses regardless of resolvability', () => {
|
|
127
|
+
const source = `
|
|
128
|
+
import { format } from './format'
|
|
129
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
130
|
+
return <p>{/* @client */ tags.map(format).join(' ')}</p>
|
|
131
|
+
}
|
|
132
|
+
export { TagLine }
|
|
133
|
+
`
|
|
134
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
135
|
+
const ir = jsxToIR(ctx)
|
|
136
|
+
const expr = textExpr(ir)
|
|
137
|
+
expect(expr.clientOnly).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('inline arrow callback is unaffected (pre-existing #2073 shape)', () => {
|
|
141
|
+
const source = `
|
|
142
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
143
|
+
return <p>{tags.map(t => '#' + t).join(' ')}</p>
|
|
144
|
+
}
|
|
145
|
+
export { TagLine }
|
|
146
|
+
`
|
|
147
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
148
|
+
const ir = jsxToIR(ctx)
|
|
149
|
+
const expr = textExpr(ir)
|
|
150
|
+
const joinCall = expr.parsed!
|
|
151
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
152
|
+
expect(isSupported(joinCall).supported).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('resolution generalizes to other CALLBACK_METHODS (e.g. value-position .sort)', () => {
|
|
156
|
+
const source = `
|
|
157
|
+
const byLen = (a: string, b: string) => a.length - b.length
|
|
158
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
159
|
+
return <p>{tags.sort(byLen).join(' ')}</p>
|
|
160
|
+
}
|
|
161
|
+
export { TagLine }
|
|
162
|
+
`
|
|
163
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
164
|
+
const ir = jsxToIR(ctx)
|
|
165
|
+
const expr = textExpr(ir)
|
|
166
|
+
const joinCall = expr.parsed!
|
|
167
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
168
|
+
const sortCall = asCallbackMethodCall(joinCall.object)
|
|
169
|
+
expect(sortCall).not.toBeNull()
|
|
170
|
+
expect(sortCall!.arrow.params).toEqual(['a', 'b'])
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('cross-kind ambiguity (const AND function of the same name) stays refused', () => {
|
|
174
|
+
const source = `
|
|
175
|
+
const format = (t: string) => 'const:' + t
|
|
176
|
+
function format2() {}
|
|
177
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
178
|
+
function format(t: string) { return 'fn:' + t }
|
|
179
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
180
|
+
}
|
|
181
|
+
export { TagLine }
|
|
182
|
+
`
|
|
183
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
184
|
+
const ir = jsxToIR(ctx)
|
|
185
|
+
const expr = textExpr(ir)
|
|
186
|
+
const joinCall = expr.parsed!
|
|
187
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
188
|
+
// Component scope has BOTH a const `format` (module scope) and a
|
|
189
|
+
// `function format` (component scope) in play once findLocalConst /
|
|
190
|
+
// findLocalFunction search across scopes — same cross-kind ambiguity
|
|
191
|
+
// `resolveSortComparatorIdentifier` refuses for `.sort(fnref)`.
|
|
192
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
193
|
+
expect(isSupported(joinCall).supported).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('.reduce(fnref, init) resolves the callback and preserves the init arg', () => {
|
|
197
|
+
const source = `
|
|
198
|
+
const sum = (acc: number, t: string) => acc + t.length
|
|
199
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
200
|
+
return <p>{tags.reduce(sum, 0)}</p>
|
|
201
|
+
}
|
|
202
|
+
export { TagLine }
|
|
203
|
+
`
|
|
204
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
205
|
+
const ir = jsxToIR(ctx)
|
|
206
|
+
const expr = textExpr(ir)
|
|
207
|
+
const parsed = expr.parsed!
|
|
208
|
+
const reduceCall = asCallbackMethodCall(parsed)
|
|
209
|
+
expect(reduceCall).not.toBeNull()
|
|
210
|
+
expect(reduceCall!.method).toBe('reduce')
|
|
211
|
+
expect(reduceCall!.arrow.params).toEqual(['acc', 't'])
|
|
212
|
+
// The trailing `0` init arg survives resolution untouched.
|
|
213
|
+
expect(reduceCall!.args).toEqual([{ kind: 'literal', value: 0, literalType: 'number', raw: '0' }])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Fable review (#2214): a spliced-in resolved arrow's body is NOT
|
|
217
|
+
// re-walked, so a bare-identifier callback nested inside another
|
|
218
|
+
// resolved declaration stays refused — a deliberate one-hop limit (see
|
|
219
|
+
// resolveCallbackMethodFunctionReferences's docstring), pinned here so a
|
|
220
|
+
// future change to that behavior is a conscious decision, not a silent
|
|
221
|
+
// regression.
|
|
222
|
+
test('a bare-identifier callback nested inside a resolved declaration stays refused (one-hop limit)', () => {
|
|
223
|
+
const source = `
|
|
224
|
+
const inner = (t: string) => '#' + t
|
|
225
|
+
const outer = (xs: string[]) => xs.map(inner).join(',')
|
|
226
|
+
function TagLine({ rows }: { rows: string[][] }) {
|
|
227
|
+
return <p>{rows.map(outer).join(' ')}</p>
|
|
228
|
+
}
|
|
229
|
+
export { TagLine }
|
|
230
|
+
`
|
|
231
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
232
|
+
const ir = jsxToIR(ctx)
|
|
233
|
+
const expr = textExpr(ir)
|
|
234
|
+
const outerJoin = expr.parsed!
|
|
235
|
+
if (outerJoin.kind !== 'array-method') throw new Error('expected array-method')
|
|
236
|
+
const outerMap = asCallbackMethodCall(outerJoin.object)!
|
|
237
|
+
expect(outerMap).not.toBeNull()
|
|
238
|
+
// `outer`'s body resolves one hop (`xs.map(inner).join(',')`), but the
|
|
239
|
+
// nested `.map(inner)` inside that spliced body is never re-walked.
|
|
240
|
+
const innerJoin = outerMap.arrow.body
|
|
241
|
+
if (innerJoin.kind !== 'array-method') throw new Error('expected inner array-method')
|
|
242
|
+
if (innerJoin.object.kind !== 'call') throw new Error('expected inner call')
|
|
243
|
+
expect(innerJoin.object.args[0]).toEqual({ kind: 'identifier', name: 'inner' })
|
|
244
|
+
expect(asCallbackMethodCall(innerJoin.object)).toBeNull()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Fable review (#2214): the resolver must respect lexical scoping — a
|
|
248
|
+
// bare identifier in callback position that's actually bound by an
|
|
249
|
+
// ENCLOSING arrow's own parameter (not a same-file const/function) must
|
|
250
|
+
// never be resolved against the const/function tables, or SSR silently
|
|
251
|
+
// renders the wrong function while CSR (which evaluates the raw,
|
|
252
|
+
// correctly-scoped expression) renders the right one.
|
|
253
|
+
test('an enclosing callback arrow param shadows a same-named module const', () => {
|
|
254
|
+
const source = `
|
|
255
|
+
const fn = (t: string) => 'WRONG:' + t
|
|
256
|
+
function TagLine({ rows }: { rows: { tags: string[] }[] }) {
|
|
257
|
+
return <p>{rows.map(fn => fn.tags.map(fn).join(',')).join(' ')}</p>
|
|
258
|
+
}
|
|
259
|
+
export { TagLine }
|
|
260
|
+
`
|
|
261
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
262
|
+
const ir = jsxToIR(ctx)
|
|
263
|
+
const expr = textExpr(ir)
|
|
264
|
+
const outerJoin = expr.parsed!
|
|
265
|
+
if (outerJoin.kind !== 'array-method') throw new Error('expected array-method')
|
|
266
|
+
const outerMap = asCallbackMethodCall(outerJoin.object)!
|
|
267
|
+
// The outer arrow's body is `fn.tags.map(fn).join(',')` — the inner
|
|
268
|
+
// `.map(fn)` refers to the outer arrow's OWN param `fn`, not the
|
|
269
|
+
// module-scope const, so it must stay unresolved (an identifier arg).
|
|
270
|
+
const innerJoin = outerMap.arrow.body
|
|
271
|
+
if (innerJoin.kind !== 'array-method') throw new Error('expected inner array-method')
|
|
272
|
+
if (innerJoin.object.kind !== 'call') throw new Error('expected inner call')
|
|
273
|
+
expect(innerJoin.object.args[0]).toEqual({ kind: 'identifier', name: 'fn' })
|
|
274
|
+
expect(asCallbackMethodCall(innerJoin.object)).toBeNull()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('a loop item param shadows a same-named module const inside the loop body', () => {
|
|
278
|
+
const source = `
|
|
279
|
+
const format = (t: string) => 'WRONG:' + t
|
|
280
|
+
function TagLine({ fns, tags }: { fns: string[]; tags: string[] }) {
|
|
281
|
+
return <ul>{fns.map(format => <li>{tags.map(format).join(' ')}</li>)}</ul>
|
|
282
|
+
}
|
|
283
|
+
export { TagLine }
|
|
284
|
+
`
|
|
285
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
286
|
+
const ir = jsxToIR(ctx)
|
|
287
|
+
if (!ir || ir.type !== 'element') throw new Error('expected a root element')
|
|
288
|
+
const loop = ir.children.find(c => c.type === 'loop')
|
|
289
|
+
if (!loop || loop.type !== 'loop') throw new Error('expected a loop child')
|
|
290
|
+
expect(loop.param).toBe('format')
|
|
291
|
+
const li = loop.children.find(c => c.type === 'element')
|
|
292
|
+
if (!li || li.type !== 'element') throw new Error('expected the <li> element')
|
|
293
|
+
const innerExpr = li.children.find(c => c.type === 'expression')
|
|
294
|
+
if (!innerExpr || innerExpr.type !== 'expression') throw new Error('expected an expression child')
|
|
295
|
+
const joinCall = innerExpr.parsed!
|
|
296
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
297
|
+
// `tags.map(format)` inside the loop body refers to the loop's own item
|
|
298
|
+
// variable `format`, not the module-scope const — must stay unresolved.
|
|
299
|
+
if (joinCall.object.kind !== 'call') throw new Error('expected inner call')
|
|
300
|
+
expect(joinCall.object.args[0]).toEqual({ kind: 'identifier', name: 'format' })
|
|
301
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
302
|
+
})
|
|
303
|
+
})
|