@barefootjs/jsx 0.6.0 → 0.7.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/parsed-expr-emitter.d.ts +14 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +137 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +335 -5
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts +4 -0
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +46 -2
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +8 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +7 -7
- package/src/__tests__/child-components-in-map.test.ts +84 -0
- package/src/__tests__/client-js-generation.test.ts +51 -0
- package/src/__tests__/compiler-stress-1244.test.ts +43 -0
- package/src/__tests__/expression-parser.test.ts +109 -1
- package/src/__tests__/foreach-client-only.test.ts +80 -0
- package/src/__tests__/ir-async.test.ts +64 -0
- package/src/__tests__/ir-dynamic-tag.test.ts +104 -0
- package/src/__tests__/ir-reduce-op.test.ts +51 -0
- package/src/__tests__/reduce-op.test.ts +201 -0
- package/src/adapters/parsed-expr-emitter.ts +43 -1
- package/src/expression-parser.ts +570 -4
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +27 -4
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +55 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +47 -1
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +69 -0
- package/src/ir-to-client-js/types.ts +8 -1
- package/src/jsx-to-ir.ts +69 -0
- package/src/types.ts +9 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the `IRComponent.dynamicTag` flag — the additive signal the Go
|
|
3
|
+
* template adapter uses to lower a runtime-chosen tag (`const Tag =
|
|
4
|
+
* children.tag`) to a children passthrough instead of an impossible
|
|
5
|
+
* `{{template "Tag" ...}}` call.
|
|
6
|
+
*
|
|
7
|
+
* Background: the `Slot` component does
|
|
8
|
+
* `const Tag = children.tag; return <Tag {...}>{...}</Tag>`
|
|
9
|
+
* The compiler treats `<Tag>` as a PascalCase component reference, so a
|
|
10
|
+
* naive Go lowering emits `{{template "Tag" .TagSlot0}}` — a template
|
|
11
|
+
* that can never be registered. Go's html/template escape-walks ALL
|
|
12
|
+
* registered templates (even dead branches), so the whole render fails
|
|
13
|
+
* with `no such template "Tag"`. `dynamicTag` lets the Go adapter detect
|
|
14
|
+
* and defuse this; Hono/CSR/Mojo ignore the flag.
|
|
15
|
+
*
|
|
16
|
+
* The binding lives inside an `if (isValidElement(children)) { … }`
|
|
17
|
+
* block, so detection must scan nested scopes (not just component-body
|
|
18
|
+
* `localConstants`). These tests pin both that positive detection and
|
|
19
|
+
* the negative case — a real imported `<Button>` must NOT be flagged.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, test, expect } from 'bun:test'
|
|
23
|
+
import { compileJSX } from '../compiler'
|
|
24
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
25
|
+
import type { IRNode, IRComponent } from '../types'
|
|
26
|
+
|
|
27
|
+
const adapter = new TestAdapter()
|
|
28
|
+
|
|
29
|
+
function compileToIR(source: string) {
|
|
30
|
+
const result = compileJSX(source, 'demo.tsx', { adapter, outputIR: true })
|
|
31
|
+
expect(result.errors.filter(e => e.severity === 'error')).toEqual([])
|
|
32
|
+
const ir = result.files.find(f => f.type === 'ir')!
|
|
33
|
+
return JSON.parse(ir.content)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findComponent(node: IRNode, name: string): IRComponent | null {
|
|
37
|
+
if (node.type === 'component' && (node as IRComponent).name === name) {
|
|
38
|
+
return node as IRComponent
|
|
39
|
+
}
|
|
40
|
+
const anyNode = node as IRNode & {
|
|
41
|
+
children?: IRNode[]
|
|
42
|
+
consequent?: IRNode
|
|
43
|
+
alternate?: IRNode
|
|
44
|
+
then?: IRNode
|
|
45
|
+
else?: IRNode
|
|
46
|
+
}
|
|
47
|
+
for (const key of ['children', 'consequent', 'alternate', 'then', 'else'] as const) {
|
|
48
|
+
const v = anyNode[key]
|
|
49
|
+
if (!v) continue
|
|
50
|
+
const list = Array.isArray(v) ? v : [v]
|
|
51
|
+
for (const c of list) {
|
|
52
|
+
const found = findComponent(c, name)
|
|
53
|
+
if (found) return found
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('IRComponent.dynamicTag', () => {
|
|
60
|
+
test('a block-scoped `const Tag = children.tag` JSX tag is flagged dynamicTag', () => {
|
|
61
|
+
const ir = compileToIR(`
|
|
62
|
+
function isValidElement(el: unknown): el is { tag: unknown; props: Record<string, unknown> } {
|
|
63
|
+
return !!(el && typeof el === 'object' && 'tag' in el && 'props' in el)
|
|
64
|
+
}
|
|
65
|
+
export function Slot({ children, className, ...props }: { children?: any; className?: string; [k: string]: unknown }) {
|
|
66
|
+
if (children && isValidElement(children)) {
|
|
67
|
+
const Tag = children.tag as any
|
|
68
|
+
const childProps = children.props
|
|
69
|
+
return <Tag {...childProps} {...props} className={className}>{childProps.children}</Tag>
|
|
70
|
+
}
|
|
71
|
+
return <>{children}</>
|
|
72
|
+
}
|
|
73
|
+
`)
|
|
74
|
+
const tag = findComponent(ir.root, 'Tag')
|
|
75
|
+
expect(tag).not.toBeNull()
|
|
76
|
+
expect(tag!.dynamicTag).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('a real imported component is NOT flagged dynamicTag', () => {
|
|
80
|
+
const ir = compileToIR(`
|
|
81
|
+
import { Button } from './button'
|
|
82
|
+
export function Demo() {
|
|
83
|
+
return <Button>hi</Button>
|
|
84
|
+
}
|
|
85
|
+
`)
|
|
86
|
+
const button = findComponent(ir.root, 'Button')
|
|
87
|
+
expect(button).not.toBeNull()
|
|
88
|
+
expect(button!.dynamicTag).toBeUndefined()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('a local JSX-producing const factory is NOT flagged dynamicTag', () => {
|
|
92
|
+
const ir = compileToIR(`
|
|
93
|
+
export function Demo() {
|
|
94
|
+
const Inner = () => <span>x</span>
|
|
95
|
+
return <div><Inner /></div>
|
|
96
|
+
}
|
|
97
|
+
`)
|
|
98
|
+
const inner = findComponent(ir.root, 'Inner')
|
|
99
|
+
// Local component factories lower via the jsx* inlining paths; whether
|
|
100
|
+
// they survive as an IRComponent node or get inlined, they must never
|
|
101
|
+
// carry the dynamicTag flag (their initializer is an arrow, not `.tag`).
|
|
102
|
+
if (inner) expect(inner.dynamicTag).toBeUndefined()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { analyzeComponent } from '../analyzer'
|
|
3
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
4
|
+
import { parseExpression } from '../expression-parser'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* IR-level verification for `.reduce(fn, init)` (#1448 Tier C). The
|
|
8
|
+
* parser intercepts the arithmetic-fold catalogue into a structured
|
|
9
|
+
* `array-method` + `ReduceOp` node — this test pins the source-string
|
|
10
|
+
* that survives through `jsxToIR` to an `IRExpression`, then re-parses
|
|
11
|
+
* it to confirm the structured fold shape adapters consume.
|
|
12
|
+
*
|
|
13
|
+
* Hydration and template-emit correctness are pinned at the adapter
|
|
14
|
+
* conformance layer (`reduce-*` fixtures in `packages/adapter-tests/
|
|
15
|
+
* fixtures/methods/`).
|
|
16
|
+
*/
|
|
17
|
+
describe('reduce(fn, init) IR shape', () => {
|
|
18
|
+
test('sum over a struct field re-parses to a numeric ReduceOp', () => {
|
|
19
|
+
const source = `
|
|
20
|
+
'use client'
|
|
21
|
+
import { createSignal } from '@barefootjs/client'
|
|
22
|
+
|
|
23
|
+
export function Total() {
|
|
24
|
+
const [items] = createSignal<{ duration: number }[]>([])
|
|
25
|
+
return <div>{items().reduce((sum, t) => sum + t.duration, 0)}</div>
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
const ctx = analyzeComponent(source, 'Total.tsx')
|
|
30
|
+
const ir = jsxToIR(ctx)
|
|
31
|
+
|
|
32
|
+
expect(ir).not.toBeNull()
|
|
33
|
+
if (ir!.type !== 'element') return
|
|
34
|
+
const exprNode = ir!.children.find(c => c.type === 'expression')
|
|
35
|
+
expect(exprNode?.type).toBe('expression')
|
|
36
|
+
if (exprNode?.type !== 'expression') return
|
|
37
|
+
|
|
38
|
+
// The IR carries the expression as a source string (the same shape
|
|
39
|
+
// adapters re-parse at emit time). Round-trip it to confirm the
|
|
40
|
+
// ReduceOp catalogue match survives the analyzer → IR boundary.
|
|
41
|
+
const parsed = parseExpression(exprNode.expr)
|
|
42
|
+
expect(parsed.kind).toBe('array-method')
|
|
43
|
+
if (parsed.kind !== 'array-method') return
|
|
44
|
+
expect(parsed.method).toBe('reduce')
|
|
45
|
+
if (parsed.method !== 'reduce') return
|
|
46
|
+
expect(parsed.reduceOp.op).toBe('+')
|
|
47
|
+
expect(parsed.reduceOp.key).toEqual({ kind: 'field', field: 'duration' })
|
|
48
|
+
expect(parsed.reduceOp.type).toBe('numeric')
|
|
49
|
+
expect(parsed.reduceOp.init).toBe('0')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { parseExpression, isSupported, stringifyParsedExpr } from '../expression-parser'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `.reduce(fn, init)` arithmetic-fold catalogue (#1448 Tier C). The
|
|
6
|
+
* parser intercepts the accepted shapes into a structured `ReduceOp`
|
|
7
|
+
* (mirroring the `.sort` `SortComparator` precedent) and refuses
|
|
8
|
+
* everything else to `unsupported` so the template adapters surface
|
|
9
|
+
* BF101 with the `@client` escape hatch.
|
|
10
|
+
*/
|
|
11
|
+
describe('reduce() arithmetic-fold catalogue', () => {
|
|
12
|
+
test('numeric sum over a field → field/numeric', () => {
|
|
13
|
+
const r = parseExpression('items.reduce((sum, t) => sum + t.duration, 0)')
|
|
14
|
+
expect(r.kind).toBe('array-method')
|
|
15
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
16
|
+
expect(r.reduceOp.op).toBe('+')
|
|
17
|
+
expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'duration' })
|
|
18
|
+
expect(r.reduceOp.type).toBe('numeric')
|
|
19
|
+
expect(r.reduceOp.init).toBe('0')
|
|
20
|
+
expect(r.reduceOp.paramAcc).toBe('sum')
|
|
21
|
+
expect(r.reduceOp.paramItem).toBe('t')
|
|
22
|
+
}
|
|
23
|
+
expect(isSupported(r).supported).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('numeric sum over self (primitive array) → self/numeric', () => {
|
|
27
|
+
const r = parseExpression('nums.reduce((a, b) => a + b, 0)')
|
|
28
|
+
expect(r.kind).toBe('array-method')
|
|
29
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
30
|
+
expect(r.reduceOp.op).toBe('+')
|
|
31
|
+
expect(r.reduceOp.key).toEqual({ kind: 'self' })
|
|
32
|
+
expect(r.reduceOp.type).toBe('numeric')
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('reduceRight shares the catalogue and preserves the method name', () => {
|
|
37
|
+
const r = parseExpression("items.reduceRight((acc, x) => acc + x.label, '')")
|
|
38
|
+
expect(r.kind).toBe('array-method')
|
|
39
|
+
if (r.kind === 'array-method') {
|
|
40
|
+
expect(r.method).toBe('reduceRight')
|
|
41
|
+
if (r.method === 'reduceRight') {
|
|
42
|
+
expect(r.reduceOp.op).toBe('+')
|
|
43
|
+
expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'label' })
|
|
44
|
+
expect(r.reduceOp.type).toBe('string')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('reduceRight round-trips with its method name preserved', () => {
|
|
50
|
+
const r = parseExpression('nums.reduceRight((a, b) => a + b, 0)')
|
|
51
|
+
expect(stringifyParsedExpr(r)).toBe('nums.reduceRight((a,b) => a + b, 0)')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('product over a field with init 1 → field/numeric/*', () => {
|
|
55
|
+
const r = parseExpression('items.reduce((acc, x) => acc * x.qty, 1)')
|
|
56
|
+
expect(r.kind).toBe('array-method')
|
|
57
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
58
|
+
expect(r.reduceOp.op).toBe('*')
|
|
59
|
+
expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'qty' })
|
|
60
|
+
expect(r.reduceOp.type).toBe('numeric')
|
|
61
|
+
expect(r.reduceOp.init).toBe('1')
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('string init makes + a concatenation fold (init is the decoded value)', () => {
|
|
66
|
+
const r = parseExpression("items.reduce((acc, x) => acc + x.label, '')")
|
|
67
|
+
expect(r.kind).toBe('array-method')
|
|
68
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
69
|
+
expect(r.reduceOp.op).toBe('+')
|
|
70
|
+
expect(r.reduceOp.type).toBe('string')
|
|
71
|
+
expect(r.reduceOp.init).toBe('') // decoded contents, not the `''` source
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('non-empty string seed decodes to its contents', () => {
|
|
76
|
+
const r = parseExpression("items.reduce((acc, x) => acc + x.label, ', ')")
|
|
77
|
+
expect(r.kind).toBe('array-method')
|
|
78
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
79
|
+
expect(r.reduceOp.init).toBe(', ')
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('numeric init normalises separators / radix to canonical decimal', () => {
|
|
84
|
+
// `.text` gives TS's canonical decimal so Go ParseFloat + Perl agree
|
|
85
|
+
// (#1728 review: raw `1_000` / `0x10` would mis-parse on Go).
|
|
86
|
+
for (const [src, expected] of [
|
|
87
|
+
['1_000', '1000'],
|
|
88
|
+
['0x10', '16'],
|
|
89
|
+
['1e3', '1000'],
|
|
90
|
+
] as const) {
|
|
91
|
+
const r = parseExpression(`nums.reduce((a, b) => a + b, ${src})`)
|
|
92
|
+
expect(r.kind).toBe('array-method')
|
|
93
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
94
|
+
expect(r.reduceOp.init).toBe(expected)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('parenthesized numeric init is unwrapped and accepted', () => {
|
|
100
|
+
for (const [src, expected] of [
|
|
101
|
+
['(0)', '0'],
|
|
102
|
+
['(-1)', '-1'],
|
|
103
|
+
] as const) {
|
|
104
|
+
const r = parseExpression(`nums.reduce((a, b) => a + b, ${src})`)
|
|
105
|
+
expect(r.kind).toBe('array-method')
|
|
106
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
107
|
+
expect(r.reduceOp.init).toBe(expected)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('an escape-free seed containing an apostrophe is accepted (decoded contents kept)', () => {
|
|
113
|
+
// `"a'b"` is escape-free (decoded === raw inner), so it's accepted;
|
|
114
|
+
// the decoded value carries the apostrophe, which the Mojo emit
|
|
115
|
+
// single-quote-escapes.
|
|
116
|
+
const r = parseExpression(`items.reduce((acc, x) => acc + x.l, "a'b")`)
|
|
117
|
+
expect(r.kind).toBe('array-method')
|
|
118
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
119
|
+
expect(r.reduceOp.init).toBe("a'b")
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('single-return block body unwraps to the fold expression', () => {
|
|
124
|
+
const r = parseExpression('items.reduce((sum, t) => { return sum + t.n }, 0)')
|
|
125
|
+
expect(r.kind).toBe('array-method')
|
|
126
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
127
|
+
expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'n' })
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('negative numeric init is accepted', () => {
|
|
132
|
+
const r = parseExpression('nums.reduce((a, b) => a + b, -1)')
|
|
133
|
+
expect(r.kind).toBe('array-method')
|
|
134
|
+
if (r.kind === 'array-method' && r.method === 'reduce') {
|
|
135
|
+
expect(r.reduceOp.init).toBe('-1')
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('round-trips a numeric fold back to valid JS via stringifyParsedExpr', () => {
|
|
140
|
+
const r = parseExpression('items.reduce((sum, t) => sum + t.duration, 0)')
|
|
141
|
+
expect(stringifyParsedExpr(r)).toBe('items.reduce((sum,t) => sum + t.duration, 0)')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('round-trips a string fold by re-quoting the decoded seed', () => {
|
|
145
|
+
const r = parseExpression("items.reduce((a, x) => a + x.l, ', ')")
|
|
146
|
+
// Decoded seed `, ` re-quoted via JSON.stringify → a valid JS string.
|
|
147
|
+
expect(stringifyParsedExpr(r)).toBe('items.reduce((a,x) => a + x.l, ", ")')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('refused shapes → unsupported (BF101)', () => {
|
|
151
|
+
test('missing initial value', () => {
|
|
152
|
+
// A no-init reduce isn't intercepted (it needs 2 args); it falls
|
|
153
|
+
// through to a generic `call` that the UNSUPPORTED_METHODS gate
|
|
154
|
+
// refuses — JS throws on an empty array here, so a template can't
|
|
155
|
+
// mirror it cleanly.
|
|
156
|
+
const r = parseExpression('items.reduce((sum, t) => sum + t.duration)')
|
|
157
|
+
expect(isSupported(r).supported).toBe(false)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('string concat with * is rejected', () => {
|
|
161
|
+
const r = parseExpression("items.reduce((acc, x) => acc * x.label, '')")
|
|
162
|
+
expect(r.kind).toBe('unsupported')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('accumulator on the right operand is rejected', () => {
|
|
166
|
+
const r = parseExpression('items.reduce((sum, t) => t.duration + sum, 0)')
|
|
167
|
+
expect(r.kind).toBe('unsupported')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('non-literal init is rejected', () => {
|
|
171
|
+
const r = parseExpression('items.reduce((sum, t) => sum + t.n, start)')
|
|
172
|
+
expect(r.kind).toBe('unsupported')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('object-building reducer is rejected', () => {
|
|
176
|
+
const r = parseExpression('items.reduce((acc, x) => ({ ...acc, [x.id]: x }), {})')
|
|
177
|
+
expect(r.kind).toBe('unsupported')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('deep field access is rejected', () => {
|
|
181
|
+
const r = parseExpression('items.reduce((sum, t) => sum + t.a.b, 0)')
|
|
182
|
+
expect(r.kind).toBe('unsupported')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('string seed carrying an escape is rejected (cross-adapter safety)', () => {
|
|
186
|
+
// A seed with an escape can't be guaranteed byte-equal across the
|
|
187
|
+
// Go-template / Perl string embeddings without per-target decoding,
|
|
188
|
+
// so it refuses to `unsupported` rather than risk divergence.
|
|
189
|
+
const r = parseExpression("items.reduce((acc, x) => acc + x.l, '\\n')")
|
|
190
|
+
expect(r.kind).toBe('unsupported')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('reduceRight without an init is refused (like reduce)', () => {
|
|
194
|
+
// The matching 2-arg form is intercepted; a no-init reduceRight
|
|
195
|
+
// falls through to a generic call the UNSUPPORTED_METHODS gate
|
|
196
|
+
// refuses (JS throws on an empty array there).
|
|
197
|
+
const rr = parseExpression('items.reduceRight((sum, t) => sum + t.n)')
|
|
198
|
+
expect(isSupported(rr).supported).toBe(false)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* be added in one place.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import type { ParsedExpr, SortComparator, TemplatePart } from '../expression-parser'
|
|
36
|
+
import type { ParsedExpr, SortComparator, ReduceOp, FlatDepth, FlatMapOp, TemplatePart } from '../expression-parser'
|
|
37
37
|
|
|
38
38
|
export type HigherOrderMethod = 'filter' | 'every' | 'some' | 'find' | 'findIndex' | 'findLast' | 'findLastIndex'
|
|
39
39
|
|
|
@@ -82,6 +82,17 @@ export type ArrayMethod =
|
|
|
82
82
|
*/
|
|
83
83
|
export type SortMethod = 'sort' | 'toSorted'
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* `reduce` / `reduceRight` are handled by the dedicated `reduceMethod()`
|
|
87
|
+
* dispatcher arm (#1448 Tier C) for the same reason sort is: they carry
|
|
88
|
+
* a structured `ReduceOp` (the parsed arithmetic-fold spec) rather than
|
|
89
|
+
* a `ParsedExpr[]` args list, so folding them into `arrayMethod()` would
|
|
90
|
+
* force a spec-or-args runtime check at every call site. The method name
|
|
91
|
+
* is threaded through so adapters can pick the fold direction (left for
|
|
92
|
+
* `reduce`, right for `reduceRight`).
|
|
93
|
+
*/
|
|
94
|
+
export type ReduceMethod = 'reduce' | 'reduceRight'
|
|
95
|
+
|
|
85
96
|
export type LiteralType = 'string' | 'number' | 'boolean' | 'null'
|
|
86
97
|
|
|
87
98
|
/**
|
|
@@ -140,6 +151,28 @@ export interface ParsedExprEmitter {
|
|
|
140
151
|
comparator: SortComparator,
|
|
141
152
|
emit: (e: ParsedExpr) => string,
|
|
142
153
|
): string
|
|
154
|
+
reduceMethod(
|
|
155
|
+
method: ReduceMethod,
|
|
156
|
+
object: ParsedExpr,
|
|
157
|
+
reduceOp: ReduceOp,
|
|
158
|
+
emit: (e: ParsedExpr) => string,
|
|
159
|
+
): string
|
|
160
|
+
// `.flat(depth?)` gets its own dispatcher arm (#1448 Tier C): it carries
|
|
161
|
+
// a structured `FlatDepth` (the validated literal / `'infinity'`) rather
|
|
162
|
+
// than a `ParsedExpr[]` args list, same rationale as sort / reduce.
|
|
163
|
+
flatMethod(
|
|
164
|
+
object: ParsedExpr,
|
|
165
|
+
depth: FlatDepth,
|
|
166
|
+
emit: (e: ParsedExpr) => string,
|
|
167
|
+
): string
|
|
168
|
+
// `.flatMap(fn)` value-returning field projection gets its own arm
|
|
169
|
+
// (#1448 Tier C): it carries a structured `FlatMapOp` rather than a
|
|
170
|
+
// `ParsedExpr[]` args list, same rationale as sort / reduce / flat.
|
|
171
|
+
flatMapMethod(
|
|
172
|
+
object: ParsedExpr,
|
|
173
|
+
op: FlatMapOp,
|
|
174
|
+
emit: (e: ParsedExpr) => string,
|
|
175
|
+
): string
|
|
143
176
|
unsupported(raw: string, reason: string): string
|
|
144
177
|
}
|
|
145
178
|
|
|
@@ -183,6 +216,15 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
183
216
|
if (expr.method === 'sort' || expr.method === 'toSorted') {
|
|
184
217
|
return emitter.sortMethod(expr.method, expr.object, expr.comparator, emit)
|
|
185
218
|
}
|
|
219
|
+
if (expr.method === 'reduce' || expr.method === 'reduceRight') {
|
|
220
|
+
return emitter.reduceMethod(expr.method, expr.object, expr.reduceOp, emit)
|
|
221
|
+
}
|
|
222
|
+
if (expr.method === 'flat') {
|
|
223
|
+
return emitter.flatMethod(expr.object, expr.flatDepth, emit)
|
|
224
|
+
}
|
|
225
|
+
if (expr.method === 'flatMap') {
|
|
226
|
+
return emitter.flatMapMethod(expr.object, expr.flatMapOp, emit)
|
|
227
|
+
}
|
|
186
228
|
return emitter.arrayMethod(expr.method, expr.object, expr.args, emit)
|
|
187
229
|
case 'unsupported':
|
|
188
230
|
return emitter.unsupported(expr.raw, expr.reason)
|