@barefootjs/rust 0.31.4 → 0.31.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/dist/adapter/index.js +14 -44
- package/dist/adapter/minijinja-adapter.d.ts +33 -37
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +25 -49
- package/dist/vite.js +145 -193
- package/package.json +5 -5
- package/src/__tests__/minijinja-adapter-unit.test.ts +58 -20
- package/src/adapter/minijinja-adapter.ts +78 -87
- package/src/conformance-pins.ts +14 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/rust",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.6",
|
|
4
4
|
"description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"directory": "packages/adapter-rust"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@barefootjs/shared": "0.31.
|
|
57
|
+
"@barefootjs/shared": "0.31.6"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
74
|
-
"@barefootjs/jsx": "0.31.
|
|
75
|
-
"@barefootjs/vite": "0.31.
|
|
76
|
-
"@barefootjs/client": "0.31.
|
|
74
|
+
"@barefootjs/jsx": "0.31.6",
|
|
75
|
+
"@barefootjs/vite": "0.31.6",
|
|
76
|
+
"@barefootjs/client": "0.31.6",
|
|
77
77
|
"typescript": "^5.0.0",
|
|
78
78
|
"vite": "^6.0.0"
|
|
79
79
|
}
|
|
@@ -416,10 +416,11 @@ export function Parent() {
|
|
|
416
416
|
// `ir.metadata.localConstants` with no notion of AST scope — it used to
|
|
417
417
|
// substitute an outer const's literal value even at an occurrence that is
|
|
418
418
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
419
|
-
// iteration rendered the same hard-coded literal. Guarded
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
419
|
+
// iteration rendered the same hard-coded literal. Guarded via the threaded,
|
|
420
|
+
// position-accurate `BindingScope` (#2482 Stage 2 — previously a coarse
|
|
421
|
+
// whole-component exclusion, same as #2212's, that also suppressed a
|
|
422
|
+
// genuinely non-shadowed occurrence outside the loop; the threaded scope
|
|
423
|
+
// only shadows AT the enclosing loop's own body).
|
|
423
424
|
describe('MinijinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
424
425
|
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
425
426
|
const { template } = compileAndGenerate(`
|
|
@@ -458,11 +459,11 @@ function Widget({ values }: { values: number[] }) {
|
|
|
458
459
|
expect(template).toContain('1 + 5')
|
|
459
460
|
})
|
|
460
461
|
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
test('a const referenced outside the loop whose name is loop-bound elsewhere
|
|
462
|
+
// Position-accurate scope (#2482 Stage 2): a name that is loop-bound
|
|
463
|
+
// elsewhere in the component but NOT at THIS occurrence still inlines
|
|
464
|
+
// here — the coarse whole-component exclusion this used to hit (and
|
|
465
|
+
// accept as a trade-off) is gone; only the loop's own body shadows.
|
|
466
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines outside, stays the identifier inside', () => {
|
|
466
467
|
const { template } = compileAndGenerate(`
|
|
467
468
|
function Widget({ values }: { values: number[] }) {
|
|
468
469
|
const label: string = 'x'
|
|
@@ -472,7 +473,7 @@ function Widget({ values }: { values: number[] }) {
|
|
|
472
473
|
</div>
|
|
473
474
|
}
|
|
474
475
|
`)
|
|
475
|
-
expect(template).
|
|
476
|
+
expect(template).toContain("1 + 'x'")
|
|
476
477
|
expect(template).toContain('2 + label')
|
|
477
478
|
})
|
|
478
479
|
})
|
|
@@ -484,9 +485,8 @@ function Widget({ values }: { values: number[] }) {
|
|
|
484
485
|
// substitute the outer const's member value even at an occurrence that is
|
|
485
486
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
486
487
|
// iteration rendered the same hard-coded literal instead of the per-item
|
|
487
|
-
// value. Guarded
|
|
488
|
-
//
|
|
489
|
-
// never inlines, falling back to the bare `cfg.x` member expression.
|
|
488
|
+
// value. Guarded via the threaded, position-accurate `BindingScope` (#2482
|
|
489
|
+
// Stage 2), same as #2221's fix above.
|
|
490
490
|
describe('MinijinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
491
491
|
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
492
492
|
const { template } = compileAndGenerate(`
|
|
@@ -511,12 +511,12 @@ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
|
511
511
|
expect(template).toContain("bf.string('bg-ghost')")
|
|
512
512
|
})
|
|
513
513
|
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
// inlines its member
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
test('a record member referenced outside the loop whose object name is loop-bound elsewhere
|
|
514
|
+
// Position-accurate scope (#2482 Stage 2): an object name that is
|
|
515
|
+
// loop-bound elsewhere in the component but NOT at THIS occurrence still
|
|
516
|
+
// inlines its member lookup here — the coarse whole-component exclusion
|
|
517
|
+
// this used to hit (and accept as a trade-off) is gone; only the loop's
|
|
518
|
+
// own body shadows.
|
|
519
|
+
test('a record member referenced outside the loop whose object name is loop-bound elsewhere still inlines outside, stays the member expression inside', () => {
|
|
520
520
|
const { template } = compileAndGenerate(`
|
|
521
521
|
const cfg = { x: 'outer-lit' }
|
|
522
522
|
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
@@ -526,11 +526,49 @@ function Widget({ rows }: { rows: { x: string }[] }) {
|
|
|
526
526
|
</div>
|
|
527
527
|
}
|
|
528
528
|
`)
|
|
529
|
-
expect(template).
|
|
529
|
+
expect(template).toContain("bf.string('outer-lit')")
|
|
530
530
|
expect(template).toContain('bf.string(cfg.x)')
|
|
531
531
|
})
|
|
532
532
|
})
|
|
533
533
|
|
|
534
|
+
// Copilot review on #2600 (#2482 Stage 2 follow-up): the position-accurate
|
|
535
|
+
// `this.scope` (replacing the old coarse `staticLoopSourceBoundNames` set)
|
|
536
|
+
// was only threaded around `renderChildren(loop.children)` in `renderLoop`
|
|
537
|
+
// — narrower than the coarse set's always-on coverage. Two row-context
|
|
538
|
+
// conversions sit OUTSIDE that window: a `.map()` preamble local's own
|
|
539
|
+
// initializer, and the whole-item-conditional `loop-i:` key anchor
|
|
540
|
+
// (`loop.key`). Both genuinely evaluate PER ROW and must see the row's own
|
|
541
|
+
// bindings — a loop param shadowing a same-named outer const must resolve
|
|
542
|
+
// to the row value there too, exactly as it already does inside the loop
|
|
543
|
+
// body proper. `renderLoop` now enters the row scope before converting
|
|
544
|
+
// either and pops it after, closing the gap.
|
|
545
|
+
describe('MinijinjaAdapter - row scope covers preamble/key conversions outside renderChildren (#2482 Stage 2 Copilot follow-up)', () => {
|
|
546
|
+
test('a loop param shadowing an outer const resolves to the row value inside a whole-item-conditional loop-i: key anchor', () => {
|
|
547
|
+
const { template } = compileAndGenerate(`
|
|
548
|
+
function Widget({ values }: { values: number[] }) {
|
|
549
|
+
const label: string = 'x'
|
|
550
|
+
return <ul>{values.map((label) => (label > 0 ? <li key={label}>{label}</li> : null))}</ul>
|
|
551
|
+
}
|
|
552
|
+
`)
|
|
553
|
+
expect(template).toContain('bf.comment("loop-i:" ~ bf.string(label))')
|
|
554
|
+
expect(template).not.toContain("bf.comment(\"loop-i:\" ~ bf.string('x'))")
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
test('a loop param shadowing an outer const resolves to the row value inside a .map() preamble local initializer', () => {
|
|
558
|
+
const { template } = compileAndGenerate(`
|
|
559
|
+
function Widget({ values }: { values: number[] }) {
|
|
560
|
+
const label: string = 'x'
|
|
561
|
+
return <ul>{values.map((label) => {
|
|
562
|
+
const cls = label
|
|
563
|
+
return <li key={label} class={cls}>{label}</li>
|
|
564
|
+
})}</ul>
|
|
565
|
+
}
|
|
566
|
+
`)
|
|
567
|
+
expect(template).toContain('{% set cls = label %}')
|
|
568
|
+
expect(template).not.toContain("{% set cls = 'x' %}")
|
|
569
|
+
})
|
|
570
|
+
})
|
|
571
|
+
|
|
534
572
|
describe('MinijinjaAdapter - fragment-root scope comment end marker (#2289)', () => {
|
|
535
573
|
// A fragment-rooted component (multiple top-level nodes) uses a
|
|
536
574
|
// comment-based scope marker instead of a real DOM element, so it needs a
|
|
@@ -170,8 +170,8 @@ import {
|
|
|
170
170
|
dangerousInnerHtmlMetacharViolation,
|
|
171
171
|
dangerousInnerHtmlDiagnostic,
|
|
172
172
|
resolveStaticLoopSource,
|
|
173
|
-
collectLoopBoundNames,
|
|
174
173
|
derivesScopeFromSlot,
|
|
174
|
+
BindingScope,
|
|
175
175
|
} from '@barefootjs/jsx'
|
|
176
176
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
177
177
|
import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
|
|
@@ -320,30 +320,24 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
320
320
|
private localConstants: IRMetadata['localConstants'] = []
|
|
321
321
|
|
|
322
322
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
323
|
+
* The one canonical, position-accurate "names bound by an enclosing loop
|
|
324
|
+
* callback" service (#2482 Stage 2) — replaces the two ad-hoc devices
|
|
325
|
+
* this adapter used to carry side by side: a coarse whole-component
|
|
326
|
+
* shadow-name Set (built once at `generate()` entry, used only to guard
|
|
327
|
+
* static-const inlining) and a ref-counted, position-accurate live map
|
|
328
|
+
* (pushed/popped around `renderLoop`'s body, used for the boolean-prop/
|
|
329
|
+
* nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
330
|
+
* local-const fallback (#2489)). Both consulted the "is this name
|
|
331
|
+
* loop-bound" question with DIFFERENT meanings — the coarse/live drift
|
|
332
|
+
* #2482 Stage 2 ends. Every shadow-guard site now reads this ONE
|
|
333
|
+
* threaded, immutable scope: `enterLoopRow`/pop-by-reference around
|
|
334
|
+
* `renderChildren(loop.children)` in `renderLoop`, mirroring the Stage
|
|
335
|
+
* 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts`. `IRLoop` already
|
|
336
|
+
* structurally satisfies `LoopBindingSource`
|
|
337
|
+
* (`param`/`index`/`paramBindings`/`preamble`), so `renderLoop` passes
|
|
338
|
+
* the loop node straight to `enterLoopRow`.
|
|
330
339
|
*/
|
|
331
|
-
private
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Names currently bound by an enclosing loop body — the block-param
|
|
335
|
-
* locals `renderLoop` introduces (item, index, per-binding destructure
|
|
336
|
-
* fields, `.map()` preamble declarations) — ref-counted so nested loops
|
|
337
|
-
* compose. This is the POSITION-ACCURATE twin of the coarse
|
|
338
|
-
* `staticLoopSourceBoundNames` above: that set is a whole-component
|
|
339
|
-
* union used only to guard static-const inlining (safe to over-suppress
|
|
340
|
-
* there — the fallback is just "don't inline"), whereas the boolean-prop
|
|
341
|
-
* and nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
342
|
-
* local-const fallback (#2489) need to know whether a name is loop-bound
|
|
343
|
-
* AT THIS POSITION, because for those the coarse fallback would silently
|
|
344
|
-
* mis-render a genuine occurrence of the same name outside the loop too.
|
|
345
|
-
*/
|
|
346
|
-
private loopBoundNames: Map<string, number> = new Map()
|
|
340
|
+
private scope: BindingScope = BindingScope.EMPTY
|
|
347
341
|
|
|
348
342
|
/**
|
|
349
343
|
* Optional, no-default props that are `None` when the caller omits them.
|
|
@@ -377,8 +371,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
377
371
|
// ("True"/"False") (#1897, pagination's data-active).
|
|
378
372
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
379
373
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
380
|
-
this.
|
|
381
|
-
this.loopBoundNames.clear()
|
|
374
|
+
this.scope = BindingScope.EMPTY
|
|
382
375
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
383
376
|
this.stringValueNames = collectStringValueNames(ir)
|
|
384
377
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -878,9 +871,11 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
878
871
|
// bare identifier expression below, same as before #2208 — which
|
|
879
872
|
// still trips the pre-existing BF101 gate for an unresolvable local
|
|
880
873
|
// const reference (a loud, conservative refusal, not a silent wrong
|
|
881
|
-
// value).
|
|
874
|
+
// value). Canonical, position-accurate predicate (#2482 Stage 2) — the
|
|
875
|
+
// enclosing loop scope's own membership, not a coarse whole-component
|
|
876
|
+
// union.
|
|
882
877
|
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
883
|
-
isNameShadowed:
|
|
878
|
+
isNameShadowed: this.scope.asShadowPredicate(),
|
|
884
879
|
})
|
|
885
880
|
const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null
|
|
886
881
|
|
|
@@ -896,6 +891,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
896
891
|
suggestion: {
|
|
897
892
|
message:
|
|
898
893
|
'Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client.',
|
|
894
|
+
escape: [{ kind: 'prop-precompute' }, { kind: 'client-directive' }],
|
|
899
895
|
},
|
|
900
896
|
})
|
|
901
897
|
}
|
|
@@ -985,6 +981,34 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
985
981
|
// loop-child naming convention). renderedChildren above was computed with
|
|
986
982
|
// the previous flag; recompute under the loop flag.
|
|
987
983
|
|
|
984
|
+
// This loop's row scope, for the position-accurate shadow guard
|
|
985
|
+
// (#2488/#2489, canonicalized on `BindingScope` in #2482 Stage 2).
|
|
986
|
+
// `IRLoop` already structurally satisfies `LoopBindingSource`
|
|
987
|
+
// (`param`/`index`/`paramBindings`/`preamble`) — `enterLoopRow(loop)`
|
|
988
|
+
// binds exactly the for-header target(s), each destructure binding,
|
|
989
|
+
// the index (when present), and the `.map()` preamble's declared
|
|
990
|
+
// locals, mirroring what THIS renderLoop's for-header + `indexLocalLines`
|
|
991
|
+
// actually introduce. Restored by reference (immutable — no ref-count
|
|
992
|
+
// bookkeeping) so nested loops compose for free. Entered BEFORE
|
|
993
|
+
// `preambleLines`/`bodyChildren` are converted and popped AFTER, not
|
|
994
|
+
// just around `renderChildren` — Copilot review on #2600 caught that
|
|
995
|
+
// the narrower window silently regressed the module-const shadow guard
|
|
996
|
+
// (`_resolveLiteralConst`/`_resolveStaticRecordLiteral`, now
|
|
997
|
+
// scope-driven instead of the old coarse whole-component set) for two
|
|
998
|
+
// row-context conversions that sit OUTSIDE `renderChildren`: a
|
|
999
|
+
// `.map()` preamble local's own initializer (`d.raw`, below) and the
|
|
1000
|
+
// whole-item-conditional `loop-i:` key anchor (`loop.key`, in
|
|
1001
|
+
// `bodyChildren` below) — both genuinely evaluate PER ROW and must see
|
|
1002
|
+
// the row's own bindings. `loop.filterPredicate` deliberately stays
|
|
1003
|
+
// OUTSIDE this window (further down, after the pop): per the Stage 0
|
|
1004
|
+
// design a filter/sort callback's own param is a separate `callback`
|
|
1005
|
+
// frame, never folded into the `.map()` row — and empirically
|
|
1006
|
+
// `JinjaFilterEmitter` (the filter predicate's dedicated,
|
|
1007
|
+
// self-contained emitter) never consults `this.scope` at all, so its
|
|
1008
|
+
// position relative to the pop is inert either way.
|
|
1009
|
+
const prevScope = this.scope
|
|
1010
|
+
this.scope = prevScope.enterLoopRow(loop)
|
|
1011
|
+
|
|
988
1012
|
// Per-row locals for a `.map()` callback preamble (#2447), in source
|
|
989
1013
|
// order so a later initializer sees an earlier local — same as the
|
|
990
1014
|
// source block. Phase 1 refuses the loop outright when the preamble
|
|
@@ -993,42 +1017,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
993
1017
|
const preambleLines = (loop.preamble?.declarations ?? []).map(
|
|
994
1018
|
d => `{% set ${minijinjaIdent(d.name)} = ${this.convertExpressionToJinja(d.raw, d.valueParsed)} %}`,
|
|
995
1019
|
)
|
|
996
|
-
// Names this loop binds in body scope, for the position-accurate
|
|
997
|
-
// `loopBoundNames` guard (#2488) — mirrors the ERB adapter's `loopBound`
|
|
998
|
-
// derivation, adapted to what THIS renderLoop actually binds: the
|
|
999
|
-
// for-header target(s) (`loopVar` / `objectIteration` key+value /
|
|
1000
|
-
// `iterationShape === 'keys'`'s `param`), each `indexLocalLines` name
|
|
1001
|
-
// (explicit index, or a destructure binding), and the `.map()`
|
|
1002
|
-
// preamble's declared locals. Ref-counted so nested loops compose.
|
|
1003
|
-
const loopBound: string[] = []
|
|
1004
|
-
if (loop.objectIteration === 'entries') {
|
|
1005
|
-
loopBound.push(loop.index ?? param, param)
|
|
1006
|
-
} else if (loop.objectIteration === 'keys') {
|
|
1007
|
-
// `|items` binds both slots; the unused one is a throwaway (#2488).
|
|
1008
|
-
loopBound.push(param, '__bf_v')
|
|
1009
|
-
} else if (loop.objectIteration === 'values') {
|
|
1010
|
-
loopBound.push('__bf_k', param)
|
|
1011
|
-
} else if (loop.iterationShape === 'keys') {
|
|
1012
|
-
// The header still binds the throwaway loop var; `param` is only the
|
|
1013
|
-
// index alias set beneath it (#2488).
|
|
1014
|
-
loopBound.push('__bf_item', param)
|
|
1015
|
-
} else if (supportableDestructure) {
|
|
1016
|
-
loopBound.push('__bf_item', ...(loop.paramBindings ?? []).map(b => b.name))
|
|
1017
|
-
if (loop.index) loopBound.push(loop.index)
|
|
1018
|
-
} else {
|
|
1019
|
-
loopBound.push(param)
|
|
1020
|
-
if (loop.index) loopBound.push(loop.index)
|
|
1021
|
-
}
|
|
1022
|
-
for (const d of loop.preamble?.declarations ?? []) loopBound.push(d.name)
|
|
1023
|
-
for (const n of loopBound) {
|
|
1024
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
1025
|
-
}
|
|
1026
1020
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
1027
|
-
for (const n of loopBound) {
|
|
1028
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
1029
|
-
if (c <= 0) this.loopBoundNames.delete(n)
|
|
1030
|
-
else this.loopBoundNames.set(n, c)
|
|
1031
|
-
}
|
|
1032
1021
|
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
1033
1022
|
this.inLoop = prevInLoop
|
|
1034
1023
|
void renderedChildren
|
|
@@ -1036,11 +1025,13 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1036
1025
|
// Whole-item conditional: prepend an always-present `<!--bf-loop-i:KEY-->`
|
|
1037
1026
|
// anchor before each item's (possibly empty) conditional content so the
|
|
1038
1027
|
// client's `mapArrayAnchored` can hydrate every SSR-rendered item by its
|
|
1039
|
-
// anchor.
|
|
1028
|
+
// anchor. Still under the row scope (see above) — `loop.key` is a
|
|
1029
|
+
// per-row expression.
|
|
1040
1030
|
const bodyChildren =
|
|
1041
1031
|
loop.bodyIsItemConditional && loop.key
|
|
1042
1032
|
? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}\n${childrenUnderLoop}`
|
|
1043
1033
|
: childrenUnderLoop
|
|
1034
|
+
this.scope = prevScope
|
|
1044
1035
|
|
|
1045
1036
|
const lines: string[] = []
|
|
1046
1037
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
@@ -1527,13 +1518,13 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1527
1518
|
// function-scope (`!isModule`) consts whose value is NOT itself a bare
|
|
1528
1519
|
// identifier (loop guard) are considered.
|
|
1529
1520
|
//
|
|
1530
|
-
// `
|
|
1531
|
-
// param can shadow this outer const's name (`.map((attrs) => <p
|
|
1521
|
+
// `this.scope` shadow guard (#2489): an enclosing `.map()` callback's
|
|
1522
|
+
// own param can shadow this outer const's name (`.map((attrs) => <p
|
|
1532
1523
|
// {...attrs} />)`) — without the guard this forwarded the OUTER
|
|
1533
1524
|
// const's value at every iteration instead of the per-item value.
|
|
1534
|
-
// Must be the
|
|
1535
|
-
//
|
|
1536
|
-
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.
|
|
1525
|
+
// Must be the threaded, position-accurate scope — the same name
|
|
1526
|
+
// spread at ROOT (no enclosing loop) must still resolve the const.
|
|
1527
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
|
|
1537
1528
|
const localConst = (this.localConstants ?? []).find(
|
|
1538
1529
|
c => c.name === trimmed && !c.isModule,
|
|
1539
1530
|
)
|
|
@@ -1955,9 +1946,9 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1955
1946
|
return this.booleanTypedProps.has(bare)
|
|
1956
1947
|
}
|
|
1957
1948
|
|
|
1958
|
-
/** Position-accurate loop-bound-name check — see `
|
|
1949
|
+
/** Position-accurate loop-bound-name check — see `this.scope`'s docstring. */
|
|
1959
1950
|
private isLoopBoundName(name: string): boolean {
|
|
1960
|
-
return this.
|
|
1951
|
+
return this.scope.isBound(name)
|
|
1961
1952
|
}
|
|
1962
1953
|
|
|
1963
1954
|
/**
|
|
@@ -1984,17 +1975,16 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1984
1975
|
* context, so a bare reference would resolve to Undefined.
|
|
1985
1976
|
*
|
|
1986
1977
|
* The lookup is a flat name match with no notion of AST scope, so a
|
|
1987
|
-
* name
|
|
1988
|
-
* inlines (#2221) — the occurrence may be the
|
|
1989
|
-
* binding, and substituting the outer const's
|
|
1990
|
-
* iteration with the same hard-coded literal.
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
1994
|
-
* `collectStringValueNames`.
|
|
1978
|
+
* name bound by the CURRENTLY ENCLOSING loop's item/index/destructure/
|
|
1979
|
+
* preamble param never inlines (#2221) — the occurrence may be the
|
|
1980
|
+
* loop's own (shadowing) binding, and substituting the outer const's
|
|
1981
|
+
* value there renders every iteration with the same hard-coded literal.
|
|
1982
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2) — a
|
|
1983
|
+
* same-named const elsewhere in the component, outside any loop that
|
|
1984
|
+
* shadows it, still inlines.
|
|
1995
1985
|
*/
|
|
1996
1986
|
private _resolveLiteralConst(name: string): string | null {
|
|
1997
|
-
if (this.
|
|
1987
|
+
if (this.scope.isBound(name)) return null
|
|
1998
1988
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1999
1989
|
if (c?.value === undefined) return null
|
|
2000
1990
|
const v = c.value.trim()
|
|
@@ -2012,15 +2002,16 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
2012
2002
|
* scope, so an enclosing loop callback's own param of the same name
|
|
2013
2003
|
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
2014
2004
|
* still resolved to the OUTER const's member value at every iteration
|
|
2015
|
-
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2019
|
-
* at the
|
|
2005
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
2006
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2), passed
|
|
2007
|
+
* as `lookupStaticRecordLiteral`'s required guard: a name the CURRENTLY
|
|
2008
|
+
* ENCLOSING loop binds never inlines, falling back to the bare `cfg.x`
|
|
2009
|
+
* member expression (which a minijinja `for` loop binds correctly at the
|
|
2010
|
+
* shadowed occurrence); a same-named const elsewhere, outside any loop
|
|
2011
|
+
* that shadows it, still inlines.
|
|
2020
2012
|
*/
|
|
2021
2013
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
2022
|
-
|
|
2023
|
-
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
2014
|
+
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
|
|
2024
2015
|
if (!hit) return null
|
|
2025
2016
|
return hit.kind === 'number'
|
|
2026
2017
|
? hit.text
|
package/src/conformance-pins.ts
CHANGED
|
@@ -16,6 +16,9 @@ export const conformancePins: ConformancePins = {
|
|
|
16
16
|
// JS-runtime target runs it, a DSL adapter surfaces BF021 + `/* @client */`.
|
|
17
17
|
// See spec/callback-fidelity.md.
|
|
18
18
|
'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
|
|
19
|
+
// Array-builder `.map()` body (imperative `push`-into-array preamble):
|
|
20
|
+
// BF021, with a verified `/* @client */` escape — `map-array-builder-
|
|
21
|
+
// body-client` (#2613). See that fixture's docstring.
|
|
19
22
|
'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
|
|
20
23
|
'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
|
|
21
24
|
// `.fill(value)` mutates the receiver in place — no template lowering
|
|
@@ -82,13 +85,21 @@ export const conformancePins: ConformancePins = {
|
|
|
82
85
|
// scope for #2087; tracked as a follow-up at
|
|
83
86
|
// https://github.com/piconic-ai/barefootjs/issues/2321.
|
|
84
87
|
'static-array-from-props': [
|
|
85
|
-
{
|
|
88
|
+
{
|
|
89
|
+
code: 'BF101',
|
|
90
|
+
severity: 'error',
|
|
91
|
+
issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
|
|
92
|
+
},
|
|
86
93
|
],
|
|
87
94
|
// BF101 (unresolvable computed loop array, see above) fires; BF103
|
|
88
95
|
// (imported child in the loop body) no longer does now that the
|
|
89
96
|
// conformance harness passes `siblingTemplatesRegistered: true` (#2205).
|
|
90
97
|
'static-array-from-props-with-component': [
|
|
91
|
-
{
|
|
98
|
+
{
|
|
99
|
+
code: 'BF101',
|
|
100
|
+
severity: 'error',
|
|
101
|
+
issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
|
|
102
|
+
},
|
|
92
103
|
],
|
|
93
104
|
// Rest-destructure `.map()` callbacks (#2087 Phase B): every shape now
|
|
94
105
|
// lowers natively — fixed bindings at any depth/shape via a chained
|
|
@@ -122,9 +133,7 @@ export const conformancePins: ConformancePins = {
|
|
|
122
133
|
'filter-nested-callback-predicate': [
|
|
123
134
|
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
|
|
124
135
|
],
|
|
125
|
-
'filter-nested-find-predicate': [
|
|
126
|
-
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
|
|
127
|
-
],
|
|
136
|
+
'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' }],
|
|
128
137
|
// NB: TOP-LEVEL `.find` / `.findIndex` / `.findLast` / `.findLastIndex`
|
|
129
138
|
// (text position) are NOT pinned here — like xslate (unlike mojo, which
|
|
130
139
|
// refuses them), Jinja lowers them to `bf.find_eval` / `find_index_eval`
|