@barefootjs/jinja 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 -42
- package/dist/adapter/jinja-adapter.d.ts +32 -36
- package/dist/adapter/jinja-adapter.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +25 -47
- package/dist/vite.js +145 -191
- package/package.json +5 -5
- package/src/__tests__/jinja-adapter-unit.test.ts +59 -21
- package/src/adapter/jinja-adapter.ts +77 -83
- package/src/conformance-pins.ts +14 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jinja",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.6",
|
|
4
4
|
"description": "Jinja2 adapter for BarefootJS — compiles IR to .jinja templates and ships the Python BarefootJS rendering runtime; runs under any Python web framework (Flask, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"directory": "packages/adapter-jinja"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.31.
|
|
56
|
+
"@barefootjs/shared": "0.31.6"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
73
|
-
"@barefootjs/jsx": "0.31.
|
|
74
|
-
"@barefootjs/vite": "0.31.
|
|
75
|
-
"@barefootjs/client": "0.31.
|
|
73
|
+
"@barefootjs/jsx": "0.31.6",
|
|
74
|
+
"@barefootjs/vite": "0.31.6",
|
|
75
|
+
"@barefootjs/client": "0.31.6",
|
|
76
76
|
"typescript": "^5.0.0",
|
|
77
77
|
"vite": "^6.0.0"
|
|
78
78
|
}
|
|
@@ -414,11 +414,12 @@ export function Parent() {
|
|
|
414
414
|
// `ir.metadata.localConstants` with no notion of AST scope — it used to
|
|
415
415
|
// substitute an outer const's literal value even at an occurrence that is
|
|
416
416
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
417
|
-
// iteration rendered the same hard-coded literal. Guarded
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
417
|
+
// iteration rendered the same hard-coded literal. Guarded via the threaded,
|
|
418
|
+
// position-accurate `BindingScope` (#2482 Stage 2 — previously a coarse
|
|
419
|
+
// whole-component exclusion, same as #2212's, that also suppressed a
|
|
420
|
+
// genuinely non-shadowed occurrence outside the loop; the threaded scope
|
|
421
|
+
// only shadows AT the enclosing loop's own body). SSR-only tests for the
|
|
422
|
+
// same #2222 reason as the #2212 describe below.
|
|
422
423
|
describe('JinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
423
424
|
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
424
425
|
const { template } = compileAndGenerate(`
|
|
@@ -457,11 +458,11 @@ function Widget({ values }: { values: number[] }) {
|
|
|
457
458
|
expect(template).toContain('1 + 5')
|
|
458
459
|
})
|
|
459
460
|
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
test('a const referenced outside the loop whose name is loop-bound elsewhere
|
|
461
|
+
// Position-accurate scope (#2482 Stage 2): a name that is loop-bound
|
|
462
|
+
// elsewhere in the component but NOT at THIS occurrence still inlines
|
|
463
|
+
// here — the coarse whole-component exclusion this used to hit (and
|
|
464
|
+
// accept as a trade-off) is gone; only the loop's own body shadows.
|
|
465
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines outside, stays the identifier inside', () => {
|
|
465
466
|
const { template } = compileAndGenerate(`
|
|
466
467
|
function Widget({ values }: { values: number[] }) {
|
|
467
468
|
const label: string = 'x'
|
|
@@ -471,7 +472,7 @@ function Widget({ values }: { values: number[] }) {
|
|
|
471
472
|
</div>
|
|
472
473
|
}
|
|
473
474
|
`)
|
|
474
|
-
expect(template).
|
|
475
|
+
expect(template).toContain("1 + 'x'")
|
|
475
476
|
expect(template).toContain('2 + label')
|
|
476
477
|
})
|
|
477
478
|
})
|
|
@@ -483,9 +484,8 @@ function Widget({ values }: { values: number[] }) {
|
|
|
483
484
|
// substitute the outer const's member value even at an occurrence that is
|
|
484
485
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
485
486
|
// iteration rendered the same hard-coded literal instead of the per-item
|
|
486
|
-
// value. Guarded
|
|
487
|
-
//
|
|
488
|
-
// never inlines, falling back to the bare `cfg['x']` member expression.
|
|
487
|
+
// value. Guarded via the threaded, position-accurate `BindingScope` (#2482
|
|
488
|
+
// Stage 2), same as #2221's fix above.
|
|
489
489
|
describe('JinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
490
490
|
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
491
491
|
const { template } = compileAndGenerate(`
|
|
@@ -510,12 +510,12 @@ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
|
510
510
|
expect(template).toContain("bf.string('bg-ghost')")
|
|
511
511
|
})
|
|
512
512
|
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
// inlines its member
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
test('a record member referenced outside the loop whose object name is loop-bound elsewhere
|
|
513
|
+
// Position-accurate scope (#2482 Stage 2): an object name that is
|
|
514
|
+
// loop-bound elsewhere in the component but NOT at THIS occurrence still
|
|
515
|
+
// inlines its member lookup here — the coarse whole-component exclusion
|
|
516
|
+
// this used to hit (and accept as a trade-off) is gone; only the loop's
|
|
517
|
+
// own body shadows.
|
|
518
|
+
test('a record member referenced outside the loop whose object name is loop-bound elsewhere still inlines outside, stays the member expression inside', () => {
|
|
519
519
|
const { template } = compileAndGenerate(`
|
|
520
520
|
const cfg = { x: 'outer-lit' }
|
|
521
521
|
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
@@ -525,11 +525,49 @@ function Widget({ rows }: { rows: { x: string }[] }) {
|
|
|
525
525
|
</div>
|
|
526
526
|
}
|
|
527
527
|
`)
|
|
528
|
-
expect(template).
|
|
528
|
+
expect(template).toContain("bf.string('outer-lit')")
|
|
529
529
|
expect(template).toContain("bf.string(cfg['x'])")
|
|
530
530
|
})
|
|
531
531
|
})
|
|
532
532
|
|
|
533
|
+
// Copilot review on #2600 (#2482 Stage 2 follow-up): the position-accurate
|
|
534
|
+
// `this.scope` (replacing the old coarse `staticLoopSourceBoundNames` set)
|
|
535
|
+
// was only threaded around `renderChildren(loop.children)` in `renderLoop`
|
|
536
|
+
// — narrower than the coarse set's always-on coverage. Two row-context
|
|
537
|
+
// conversions sit OUTSIDE that window: a `.map()` preamble local's own
|
|
538
|
+
// initializer, and the whole-item-conditional `loop-i:` key anchor
|
|
539
|
+
// (`loop.key`). Both genuinely evaluate PER ROW and must see the row's own
|
|
540
|
+
// bindings — a loop param shadowing a same-named outer const must resolve
|
|
541
|
+
// to the row value there too, exactly as it already does inside the loop
|
|
542
|
+
// body proper. `renderLoop` now enters the row scope before converting
|
|
543
|
+
// either and pops it after, closing the gap.
|
|
544
|
+
describe('JinjaAdapter - row scope covers preamble/key conversions outside renderChildren (#2482 Stage 2 Copilot follow-up)', () => {
|
|
545
|
+
test('a loop param shadowing an outer const resolves to the row value inside a whole-item-conditional loop-i: key anchor', () => {
|
|
546
|
+
const { template } = compileAndGenerate(`
|
|
547
|
+
function Widget({ values }: { values: number[] }) {
|
|
548
|
+
const label: string = 'x'
|
|
549
|
+
return <ul>{values.map((label) => (label > 0 ? <li key={label}>{label}</li> : null))}</ul>
|
|
550
|
+
}
|
|
551
|
+
`)
|
|
552
|
+
expect(template).toContain('bf.comment("loop-i:" ~ bf.string(label))')
|
|
553
|
+
expect(template).not.toContain("bf.comment(\"loop-i:\" ~ bf.string('x'))")
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
test('a loop param shadowing an outer const resolves to the row value inside a .map() preamble local initializer', () => {
|
|
557
|
+
const { template } = compileAndGenerate(`
|
|
558
|
+
function Widget({ values }: { values: number[] }) {
|
|
559
|
+
const label: string = 'x'
|
|
560
|
+
return <ul>{values.map((label) => {
|
|
561
|
+
const cls = label
|
|
562
|
+
return <li key={label} class={cls}>{label}</li>
|
|
563
|
+
})}</ul>
|
|
564
|
+
}
|
|
565
|
+
`)
|
|
566
|
+
expect(template).toContain('{% set cls = label %}')
|
|
567
|
+
expect(template).not.toContain("{% set cls = 'x' %}")
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
533
571
|
// A fragment-rooted component (top-level `<>...</>` return) has no single
|
|
534
572
|
// wrapping element to bound the client's scope query, so `renderFragment`
|
|
535
573
|
// brackets its children with a comment-based scope marker pair instead of
|
|
@@ -145,8 +145,8 @@ import {
|
|
|
145
145
|
dangerousInnerHtmlMetacharViolation,
|
|
146
146
|
dangerousInnerHtmlDiagnostic,
|
|
147
147
|
resolveStaticLoopSource,
|
|
148
|
-
collectLoopBoundNames,
|
|
149
148
|
derivesScopeFromSlot,
|
|
149
|
+
BindingScope,
|
|
150
150
|
} from '@barefootjs/jsx'
|
|
151
151
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
152
152
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
@@ -272,30 +272,24 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
272
272
|
private localConstants: IRMetadata['localConstants'] = []
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
275
|
+
* The one canonical, position-accurate "names bound by an enclosing loop
|
|
276
|
+
* callback" service (#2482 Stage 2) — replaces the two ad-hoc devices
|
|
277
|
+
* this adapter used to carry side by side: a coarse whole-component
|
|
278
|
+
* shadow-name Set (built once at `generate()` entry, used only to guard
|
|
279
|
+
* static-const inlining) and a ref-counted, position-accurate live map
|
|
280
|
+
* (pushed/popped around `renderLoop`'s body, used for the boolean-prop/
|
|
281
|
+
* nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
282
|
+
* local-const fallback (#2489)). Both consulted the "is this name
|
|
283
|
+
* loop-bound" question with DIFFERENT meanings — the coarse/live drift
|
|
284
|
+
* #2482 Stage 2 ends. Every shadow-guard site now reads this ONE
|
|
285
|
+
* threaded, immutable scope: `enterLoopRow`/pop-by-reference around
|
|
286
|
+
* `renderChildren(loop.children)` in `renderLoop`, mirroring the Stage
|
|
287
|
+
* 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts`. `IRLoop` already
|
|
288
|
+
* structurally satisfies `LoopBindingSource`
|
|
289
|
+
* (`param`/`index`/`paramBindings`/`preamble`), so `renderLoop` passes
|
|
290
|
+
* the loop node straight to `enterLoopRow`.
|
|
282
291
|
*/
|
|
283
|
-
private
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Names currently bound by an enclosing loop body — the block-param
|
|
287
|
-
* locals `renderLoop` introduces (item, index, per-binding destructure
|
|
288
|
-
* fields, `.map()` preamble declarations) — ref-counted so nested loops
|
|
289
|
-
* compose. This is the POSITION-ACCURATE twin of the coarse
|
|
290
|
-
* `staticLoopSourceBoundNames` above: that set is a whole-component
|
|
291
|
-
* union used only to guard static-const inlining (safe to over-suppress
|
|
292
|
-
* there — the fallback is just "don't inline"), whereas the boolean-prop
|
|
293
|
-
* and nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
294
|
-
* local-const fallback (#2489) need to know whether a name is loop-bound
|
|
295
|
-
* AT THIS POSITION, because for those the coarse fallback would silently
|
|
296
|
-
* mis-render a genuine occurrence of the same name outside the loop too.
|
|
297
|
-
*/
|
|
298
|
-
private loopBoundNames: Map<string, number> = new Map()
|
|
292
|
+
private scope: BindingScope = BindingScope.EMPTY
|
|
299
293
|
|
|
300
294
|
/**
|
|
301
295
|
* Optional, no-default props that are `None` when the caller omits them.
|
|
@@ -329,8 +323,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
329
323
|
// ("True"/"False") (#1897, pagination's data-active).
|
|
330
324
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
331
325
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
332
|
-
this.
|
|
333
|
-
this.loopBoundNames.clear()
|
|
326
|
+
this.scope = BindingScope.EMPTY
|
|
334
327
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
335
328
|
this.stringValueNames = collectStringValueNames(ir)
|
|
336
329
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -829,9 +822,11 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
829
822
|
// bare identifier expression below, same as before #2208 — which
|
|
830
823
|
// still trips the pre-existing BF101 gate for an unresolvable local
|
|
831
824
|
// const reference (a loud, conservative refusal, not a silent wrong
|
|
832
|
-
// value).
|
|
825
|
+
// value). Canonical, position-accurate predicate (#2482 Stage 2) — the
|
|
826
|
+
// enclosing loop scope's own membership, not a coarse whole-component
|
|
827
|
+
// union.
|
|
833
828
|
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
834
|
-
isNameShadowed:
|
|
829
|
+
isNameShadowed: this.scope.asShadowPredicate(),
|
|
835
830
|
})
|
|
836
831
|
const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null
|
|
837
832
|
|
|
@@ -847,6 +842,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
847
842
|
suggestion: {
|
|
848
843
|
message:
|
|
849
844
|
'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.',
|
|
845
|
+
escape: [{ kind: 'prop-precompute' }, { kind: 'client-directive' }],
|
|
850
846
|
},
|
|
851
847
|
})
|
|
852
848
|
}
|
|
@@ -941,6 +937,34 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
941
937
|
// loop-child naming convention). renderedChildren above was computed with
|
|
942
938
|
// the previous flag; recompute under the loop flag.
|
|
943
939
|
|
|
940
|
+
// This loop's row scope, for the position-accurate shadow guard
|
|
941
|
+
// (#2488/#2489, canonicalized on `BindingScope` in #2482 Stage 2).
|
|
942
|
+
// `IRLoop` already structurally satisfies `LoopBindingSource`
|
|
943
|
+
// (`param`/`index`/`paramBindings`/`preamble`) — `enterLoopRow(loop)`
|
|
944
|
+
// binds exactly the for-header target(s), each destructure binding,
|
|
945
|
+
// the index (when present), and the `.map()` preamble's declared
|
|
946
|
+
// locals, mirroring what THIS renderLoop's for-header + `indexLocalLines`
|
|
947
|
+
// actually introduce. Restored by reference (immutable — no ref-count
|
|
948
|
+
// bookkeeping) so nested loops compose for free. Entered BEFORE
|
|
949
|
+
// `preambleLines`/`bodyChildren` are converted and popped AFTER, not
|
|
950
|
+
// just around `renderChildren` — Copilot review on #2600 caught that
|
|
951
|
+
// the narrower window silently regressed the module-const shadow guard
|
|
952
|
+
// (`_resolveLiteralConst`/`_resolveStaticRecordLiteral`, now
|
|
953
|
+
// scope-driven instead of the old coarse whole-component set) for two
|
|
954
|
+
// row-context conversions that sit OUTSIDE `renderChildren`: a
|
|
955
|
+
// `.map()` preamble local's own initializer (`d.raw`, below) and the
|
|
956
|
+
// whole-item-conditional `loop-i:` key anchor (`loop.key`, in
|
|
957
|
+
// `bodyChildren` below) — both genuinely evaluate PER ROW and must see
|
|
958
|
+
// the row's own bindings. `loop.filterPredicate` deliberately stays
|
|
959
|
+
// OUTSIDE this window (further down, after the pop): per the Stage 0
|
|
960
|
+
// design a filter/sort callback's own param is a separate `callback`
|
|
961
|
+
// frame, never folded into the `.map()` row — and empirically
|
|
962
|
+
// `JinjaFilterEmitter` (the filter predicate's dedicated,
|
|
963
|
+
// self-contained emitter) never consults `this.scope` at all, so its
|
|
964
|
+
// position relative to the pop is inert either way.
|
|
965
|
+
const prevScope = this.scope
|
|
966
|
+
this.scope = prevScope.enterLoopRow(loop)
|
|
967
|
+
|
|
944
968
|
// Per-row locals for a `.map()` callback preamble (#2447), in source
|
|
945
969
|
// order so a later initializer sees an earlier local — same as the
|
|
946
970
|
// source block. Phase 1 refuses the loop outright when the preamble
|
|
@@ -949,39 +973,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
949
973
|
const preambleLines = (loop.preamble?.declarations ?? []).map(
|
|
950
974
|
d => `{% set ${jinjaIdent(d.name)} = ${this.convertExpressionToJinja(d.raw, d.valueParsed)} %}`,
|
|
951
975
|
)
|
|
952
|
-
// Names this loop binds in body scope, for the position-accurate
|
|
953
|
-
// `loopBoundNames` guard (#2488) — mirrors the ERB adapter's `loopBound`
|
|
954
|
-
// derivation, adapted to what THIS renderLoop actually binds: the
|
|
955
|
-
// for-header target(s) (`loopVar` / `objectIteration` key+value /
|
|
956
|
-
// `iterationShape === 'keys'`'s `param`), each `indexLocalLines` name
|
|
957
|
-
// (explicit index, or a destructure binding), and the `.map()`
|
|
958
|
-
// preamble's declared locals. Ref-counted so nested loops compose.
|
|
959
|
-
const loopBound: string[] = []
|
|
960
|
-
if (loop.objectIteration === 'entries') {
|
|
961
|
-
loopBound.push(loop.index ?? param, param)
|
|
962
|
-
} else if (loop.objectIteration === 'keys' || loop.objectIteration === 'values') {
|
|
963
|
-
loopBound.push(param)
|
|
964
|
-
} else if (loop.iterationShape === 'keys') {
|
|
965
|
-
// The header still binds the throwaway loop var; `param` is only the
|
|
966
|
-
// index alias set beneath it (#2488).
|
|
967
|
-
loopBound.push('__bf_item', param)
|
|
968
|
-
} else if (supportableDestructure) {
|
|
969
|
-
loopBound.push('__bf_item', ...(loop.paramBindings ?? []).map(b => b.name))
|
|
970
|
-
if (loop.index) loopBound.push(loop.index)
|
|
971
|
-
} else {
|
|
972
|
-
loopBound.push(param)
|
|
973
|
-
if (loop.index) loopBound.push(loop.index)
|
|
974
|
-
}
|
|
975
|
-
for (const d of loop.preamble?.declarations ?? []) loopBound.push(d.name)
|
|
976
|
-
for (const n of loopBound) {
|
|
977
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
978
|
-
}
|
|
979
976
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
980
|
-
for (const n of loopBound) {
|
|
981
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
982
|
-
if (c <= 0) this.loopBoundNames.delete(n)
|
|
983
|
-
else this.loopBoundNames.set(n, c)
|
|
984
|
-
}
|
|
985
977
|
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
986
978
|
this.inLoop = prevInLoop
|
|
987
979
|
void renderedChildren
|
|
@@ -989,11 +981,13 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
989
981
|
// Whole-item conditional: prepend an always-present `<!--bf-loop-i:KEY-->`
|
|
990
982
|
// anchor before each item's (possibly empty) conditional content so the
|
|
991
983
|
// client's `mapArrayAnchored` can hydrate every SSR-rendered item by its
|
|
992
|
-
// anchor.
|
|
984
|
+
// anchor. Still under the row scope (see above) — `loop.key` is a
|
|
985
|
+
// per-row expression.
|
|
993
986
|
const bodyChildren =
|
|
994
987
|
loop.bodyIsItemConditional && loop.key
|
|
995
988
|
? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}\n${childrenUnderLoop}`
|
|
996
989
|
: childrenUnderLoop
|
|
990
|
+
this.scope = prevScope
|
|
997
991
|
|
|
998
992
|
const lines: string[] = []
|
|
999
993
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
@@ -1472,13 +1466,13 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1472
1466
|
// function-scope (`!isModule`) consts whose value is NOT itself a bare
|
|
1473
1467
|
// identifier (loop guard) are considered.
|
|
1474
1468
|
//
|
|
1475
|
-
// `
|
|
1476
|
-
// param can shadow this outer const's name (`.map((attrs) => <p
|
|
1469
|
+
// `this.scope` shadow guard (#2489): an enclosing `.map()` callback's
|
|
1470
|
+
// own param can shadow this outer const's name (`.map((attrs) => <p
|
|
1477
1471
|
// {...attrs} />)`) — without the guard this forwarded the OUTER
|
|
1478
1472
|
// const's value at every iteration instead of the per-item value.
|
|
1479
|
-
// Must be the
|
|
1480
|
-
//
|
|
1481
|
-
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.
|
|
1473
|
+
// Must be the threaded, position-accurate scope — the same name
|
|
1474
|
+
// spread at ROOT (no enclosing loop) must still resolve the const.
|
|
1475
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
|
|
1482
1476
|
const localConst = (this.localConstants ?? []).find(
|
|
1483
1477
|
c => c.name === trimmed && !c.isModule,
|
|
1484
1478
|
)
|
|
@@ -1894,9 +1888,9 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1894
1888
|
return this.booleanTypedProps.has(bare)
|
|
1895
1889
|
}
|
|
1896
1890
|
|
|
1897
|
-
/** Position-accurate loop-bound-name check — see `
|
|
1891
|
+
/** Position-accurate loop-bound-name check — see `this.scope`'s docstring. */
|
|
1898
1892
|
private isLoopBoundName(name: string): boolean {
|
|
1899
|
-
return this.
|
|
1893
|
+
return this.scope.isBound(name)
|
|
1900
1894
|
}
|
|
1901
1895
|
|
|
1902
1896
|
/**
|
|
@@ -1923,17 +1917,16 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1923
1917
|
* context, so a bare reference would resolve to Undefined.
|
|
1924
1918
|
*
|
|
1925
1919
|
* The lookup is a flat name match with no notion of AST scope, so a
|
|
1926
|
-
* name
|
|
1927
|
-
* inlines (#2221) — the occurrence may be the
|
|
1928
|
-
* binding, and substituting the outer const's
|
|
1929
|
-
* iteration with the same hard-coded literal.
|
|
1930
|
-
*
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
* `collectStringValueNames`.
|
|
1920
|
+
* name bound by the CURRENTLY ENCLOSING loop's item/index/destructure/
|
|
1921
|
+
* preamble param never inlines (#2221) — the occurrence may be the
|
|
1922
|
+
* loop's own (shadowing) binding, and substituting the outer const's
|
|
1923
|
+
* value there renders every iteration with the same hard-coded literal.
|
|
1924
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2) — a
|
|
1925
|
+
* same-named const elsewhere in the component, outside any loop that
|
|
1926
|
+
* shadows it, still inlines.
|
|
1934
1927
|
*/
|
|
1935
1928
|
private _resolveLiteralConst(name: string): string | null {
|
|
1936
|
-
if (this.
|
|
1929
|
+
if (this.scope.isBound(name)) return null
|
|
1937
1930
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1938
1931
|
if (c?.value === undefined) return null
|
|
1939
1932
|
const v = c.value.trim()
|
|
@@ -1951,15 +1944,16 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1951
1944
|
* scope, so an enclosing loop callback's own param of the same name
|
|
1952
1945
|
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
1953
1946
|
* still resolved to the OUTER const's member value at every iteration
|
|
1954
|
-
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1947
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
1948
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2), passed
|
|
1949
|
+
* as `lookupStaticRecordLiteral`'s required guard: a name the CURRENTLY
|
|
1950
|
+
* ENCLOSING loop binds never inlines, falling back to the bare
|
|
1957
1951
|
* `cfg['x']` member expression (which a Jinja for-loop binds correctly
|
|
1958
|
-
* at the shadowed
|
|
1952
|
+
* at the shadowed occurrence); a same-named const elsewhere, outside any
|
|
1953
|
+
* loop that shadows it, still inlines.
|
|
1959
1954
|
*/
|
|
1960
1955
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1961
|
-
|
|
1962
|
-
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1956
|
+
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
|
|
1963
1957
|
if (!hit) return null
|
|
1964
1958
|
return hit.kind === 'number'
|
|
1965
1959
|
? hit.text
|
package/src/conformance-pins.ts
CHANGED
|
@@ -15,6 +15,9 @@ export const conformancePins: ConformancePins = {
|
|
|
15
15
|
// JS-runtime target runs it, a DSL adapter surfaces BF021 + `/* @client */`.
|
|
16
16
|
// See spec/callback-fidelity.md.
|
|
17
17
|
'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
|
|
18
|
+
// Array-builder `.map()` body (imperative `push`-into-array preamble):
|
|
19
|
+
// BF021, with a verified `/* @client */` escape — `map-array-builder-
|
|
20
|
+
// body-client` (#2613). See that fixture's docstring.
|
|
18
21
|
'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
|
|
19
22
|
'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
|
|
20
23
|
// `.fill(value)` mutates the receiver in place — no template lowering
|
|
@@ -82,13 +85,21 @@ export const conformancePins: ConformancePins = {
|
|
|
82
85
|
// source) is out of 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 / structured-path `.map()` callbacks (#2087 Phase B):
|
|
94
105
|
// `isLowerableLoopDestructure` now admits fixed bindings at any
|
|
@@ -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`
|