@barefootjs/jsx 0.31.2 → 0.31.3
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/jsx-adapter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +134 -36
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +1 -1
- package/dist/scope/binding-scope.d.ts +179 -0
- package/dist/scope/binding-scope.d.ts.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
- package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
- package/src/__tests__/binding-scope.test.ts +200 -0
- package/src/__tests__/let-type-annotation.test.ts +208 -0
- package/src/adapters/jsx-adapter.ts +32 -4
- package/src/analyzer.ts +20 -3
- package/src/index.ts +4 -0
- package/src/jsx-to-ir.ts +170 -65
- package/src/prop-rewrite.ts +1 -1
- package/src/scope/binding-scope.ts +238 -0
- package/src/types.ts +8 -0
package/src/jsx-to-ir.ts
CHANGED
|
@@ -62,6 +62,7 @@ import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
|
62
62
|
import { reconstructAsSegments } from './strip-types.ts'
|
|
63
63
|
import { templatePartsToJsExpr } from './template-parts.ts'
|
|
64
64
|
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
65
|
+
import { BindingScope } from './scope/binding-scope.ts'
|
|
65
66
|
|
|
66
67
|
// =============================================================================
|
|
67
68
|
// Transform Context
|
|
@@ -102,12 +103,18 @@ interface TransformContext {
|
|
|
102
103
|
* `_p.<local>` (#2524 CSR half: `_p` is always caller-keyed).
|
|
103
104
|
*/
|
|
104
105
|
_destructuredPropAliases?: Map<string, string> | null
|
|
105
|
-
/**
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The active `.map()`/callback binding stack (#2482 Stage 1a) — replaces
|
|
108
|
+
* the former mutable per-name `Set<string>` mutated in lockstep with
|
|
109
|
+
* `transformMapCall` entry/exit. `enterLoopRow`/`enterCallback` return a
|
|
110
|
+
* NEW `BindingScope`; restoring the saved parent reference on exit is
|
|
111
|
+
* the whole mechanism (no `.delete()` bookkeeping to get wrong).
|
|
112
|
+
*/
|
|
113
|
+
scope: BindingScope
|
|
107
114
|
/**
|
|
108
115
|
* Count of enclosing `.map()` loops (0 = outermost), incremented/
|
|
109
116
|
* decremented in lockstep with entering/leaving `transformMapCall`.
|
|
110
|
-
* Unlike `
|
|
117
|
+
* Unlike `scope` (a binding stack that can gain several bound names for
|
|
111
118
|
* ONE loop level via destructuring), this is a plain per-level
|
|
112
119
|
* counter — the single source of truth `IRLoop.depth` is stamped
|
|
113
120
|
* from, so every adapter's `data-key`/`data-key-N` suffix derives
|
|
@@ -133,8 +140,9 @@ interface TransformContext {
|
|
|
133
140
|
/**
|
|
134
141
|
* Memoized free-refs binding environment. Built lazily by
|
|
135
142
|
* `makeBindingEnv` and reused across every `resolveFreeRefs` call as
|
|
136
|
-
* long as `
|
|
137
|
-
* `
|
|
143
|
+
* long as `scope`'s bound VALUE names are unchanged. Invalidated by
|
|
144
|
+
* serializing `scope.valueBoundNames()` into `_bindingEnvLoopKey` and
|
|
145
|
+
* comparing on read.
|
|
138
146
|
*/
|
|
139
147
|
_bindingEnv?: BindingEnvironment
|
|
140
148
|
_bindingEnvLoopKey?: string
|
|
@@ -525,15 +533,21 @@ function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext)
|
|
|
525
533
|
let propNames = getDestructuredPropNames(ctx)
|
|
526
534
|
if (!propNames) return dateLowered === text ? undefined : dateLowered
|
|
527
535
|
// #2222: a name bound as an enclosing loop callback's item/index param
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
536
|
+
// (or, during the return-expression transform window, a preamble-
|
|
537
|
+
// declared local — #2482 Stage 1a Commit 2 re-enters `ctx.scope` with
|
|
538
|
+
// the preamble for that window) refers to the loop binding, not the
|
|
539
|
+
// prop, at THIS transform position — maintained by `transformMapCall`
|
|
540
|
+
// as it enters/leaves each callback, so this guard is scope-accurate
|
|
541
|
+
// rather than the coarse whole-component exclusion the SSR adapters use
|
|
542
|
+
// (#2221). This is a SHADOW-GUARD query — every `ScopeBindingSource`
|
|
543
|
+
// qualifies, so it reads all-sources `boundNames()`, not
|
|
544
|
+
// `valueBoundNames()` (see that method's doc comment on
|
|
545
|
+
// `BindingScope` for the two-consumer-classes split). Filter into a
|
|
546
|
+
// fresh set — `getDestructuredPropNames` caches its set on ctx and must
|
|
547
|
+
// not be mutated.
|
|
548
|
+
const shadowingNames = ctx.scope.boundNames()
|
|
549
|
+
if (shadowingNames.size > 0) {
|
|
550
|
+
const filtered = new Set([...propNames].filter(n => !shadowingNames.has(n)))
|
|
537
551
|
if (filtered.size === 0) return dateLowered === text ? undefined : dateLowered
|
|
538
552
|
propNames = filtered
|
|
539
553
|
}
|
|
@@ -662,7 +676,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
662
676
|
spreadIdCounter: 0,
|
|
663
677
|
isRoot: true,
|
|
664
678
|
insideComponentChildren: false,
|
|
665
|
-
|
|
679
|
+
scope: BindingScope.EMPTY,
|
|
666
680
|
loopDepth: 0,
|
|
667
681
|
patterns: {
|
|
668
682
|
signals: analyzer.signals.map(s => ({
|
|
@@ -791,21 +805,27 @@ function generateSpreadSlotId(ctx: TransformContext): string {
|
|
|
791
805
|
/**
|
|
792
806
|
* Build the binding environment for `resolveFreeRefs` from the current
|
|
793
807
|
* transform context. The analyzer's collected bindings (signals, memos,
|
|
794
|
-
* props, locals, imports) plus the active loop
|
|
795
|
-
*
|
|
796
|
-
*
|
|
808
|
+
* props, locals, imports) plus the active loop-bound VALUE names (item/
|
|
809
|
+
* index/destructure — see `BindingScope.valueBoundNames`'s doc comment
|
|
810
|
+
* for why preamble locals are excluded here, #2482 Stage 1a Commit 2)
|
|
811
|
+
* form the resolution frame; the TypeChecker, when available, is
|
|
812
|
+
* forwarded so library getters carrying the Reactive<T> brand are
|
|
813
|
+
* recognised.
|
|
797
814
|
*
|
|
798
815
|
* Memoized on `ctx`: the returned env is identity-stable as long as
|
|
799
|
-
* `
|
|
800
|
-
* binding-table cache in `free-refs.ts` warm across every
|
|
801
|
-
* the same loop scope. `
|
|
802
|
-
* visitor enters / leaves `.map()` callbacks
|
|
803
|
-
*
|
|
816
|
+
* `ctx.scope`'s bound VALUE names are unchanged, which keeps the
|
|
817
|
+
* `WeakMap`-keyed binding-table cache in `free-refs.ts` warm across every
|
|
818
|
+
* expression in the same loop scope. `ctx.scope` is REASSIGNED (never
|
|
819
|
+
* mutated) as the visitor enters / leaves `.map()` callbacks (#2482 Stage
|
|
820
|
+
* 1a), so we serialize its bound names into a key rather than relying on
|
|
821
|
+
* object identity — a sibling loop at the same nesting level would
|
|
822
|
+
* otherwise get a spurious cache miss/hit mismatch across restores.
|
|
804
823
|
*/
|
|
805
824
|
function makeBindingEnv(ctx: TransformContext): BindingEnvironment {
|
|
806
|
-
const
|
|
825
|
+
const boundNames = ctx.scope.valueBoundNames()
|
|
826
|
+
const loopKey = boundNames.size === 0
|
|
807
827
|
? ''
|
|
808
|
-
: Array.from(
|
|
828
|
+
: Array.from(boundNames).sort().join('\0')
|
|
809
829
|
if (ctx._bindingEnv && ctx._bindingEnvLoopKey === loopKey) {
|
|
810
830
|
return ctx._bindingEnv
|
|
811
831
|
}
|
|
@@ -820,9 +840,11 @@ function makeBindingEnv(ctx: TransformContext): BindingEnvironment {
|
|
|
820
840
|
localFunctions: a.localFunctions,
|
|
821
841
|
imports: a.imports,
|
|
822
842
|
ambientGlobals: a.ambientGlobals,
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
843
|
+
// `valueBoundNames()` returns a per-instance set that is never
|
|
844
|
+
// mutated (cached on the immutable `BindingScope`) — a stable
|
|
845
|
+
// snapshot even if `ctx.scope` is later reassigned by an enclosing
|
|
846
|
+
// visitor frame, which swaps the instance rather than mutating it.
|
|
847
|
+
loopParams: boundNames,
|
|
826
848
|
checker: a.checker,
|
|
827
849
|
}
|
|
828
850
|
ctx._bindingEnv = env
|
|
@@ -2169,9 +2191,17 @@ function transformExpressionInner(
|
|
|
2169
2191
|
const reactive = isReactiveExpression(exprText, ctx, expr) || isReactiveOrigin(origin)
|
|
2170
2192
|
// @client expressions always need slotId and are treated as reactive for client-side evaluation
|
|
2171
2193
|
// Expressions inside loops that reference the loop parameter need slotId
|
|
2172
|
-
// so fine-grained effects can target them for per-item signal updates
|
|
2173
|
-
|
|
2174
|
-
|
|
2194
|
+
// so fine-grained effects can target them for per-item signal updates.
|
|
2195
|
+
// REACTIVITY/slotId classifier — value bindings only (item/index/
|
|
2196
|
+
// destructure), NOT preamble locals: those already get their own
|
|
2197
|
+
// dedicated slot/region-patch machinery (#2447), so folding them in
|
|
2198
|
+
// here would double-allocate and (via `hasDynamicContent` reading this
|
|
2199
|
+
// same `reactive`-adjacent signal) move an unrelated row-root slotId
|
|
2200
|
+
// decision — see `BindingScope.valueBoundNames`'s doc comment
|
|
2201
|
+
// (#2482 Stage 1a Commit 2).
|
|
2202
|
+
const scopeValueNames = ctx.scope.valueBoundNames()
|
|
2203
|
+
const refsLoopParam = scopeValueNames.size > 0
|
|
2204
|
+
&& Array.from(scopeValueNames).some(p => new RegExp(`\\b${p}\\b`).test(exprText))
|
|
2175
2205
|
|
|
2176
2206
|
// Compute AST-derived flags. `callsReactive` recognises signal-getter / memo
|
|
2177
2207
|
// calls even inside deeper expressions (e.g., `format(count())`); `hasCalls`
|
|
@@ -4021,8 +4051,13 @@ function transformMapCall(
|
|
|
4021
4051
|
method: 'map' | 'flatMap' = 'map'
|
|
4022
4052
|
): IRLoop | null {
|
|
4023
4053
|
// Capture nesting depth before we register this map's own params.
|
|
4024
|
-
// ctx.
|
|
4025
|
-
|
|
4054
|
+
// ctx.scope is populated by the *outer* map; if any VALUE names (item/
|
|
4055
|
+
// index/destructure) are bound we are inside one. Reads
|
|
4056
|
+
// `valueBoundNames()`, not `boundNames()`, for consistency with the
|
|
4057
|
+
// other structural/reactivity consumers (#2482 Stage 1a Commit 2) —
|
|
4058
|
+
// though a bound outer-loop row always carries at least one value
|
|
4059
|
+
// binding regardless, so this can't actually change the answer.
|
|
4060
|
+
const isNested = ctx.scope.valueBoundNames().size > 0
|
|
4026
4061
|
// Diagnostic count at entry — the structural net at the scalar fallthrough
|
|
4027
4062
|
// de-dups against refusals fired DURING this call (leaf-wiring, DSL gates),
|
|
4028
4063
|
// never against unrelated diagnostics recorded before it.
|
|
@@ -4322,17 +4357,17 @@ function transformMapCall(
|
|
|
4322
4357
|
}
|
|
4323
4358
|
}
|
|
4324
4359
|
|
|
4325
|
-
// Register loop
|
|
4326
|
-
//
|
|
4327
|
-
//
|
|
4328
|
-
// `
|
|
4329
|
-
//
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4360
|
+
// Register loop-bound names so expressions referencing them get slotId,
|
|
4361
|
+
// by entering a new BindingScope frame for this row (#2482 Stage 1a).
|
|
4362
|
+
// For destructured patterns, the frame binds the individual binding
|
|
4363
|
+
// names — `\b${param}\b` never matches a bare name like `cfg` when
|
|
4364
|
+
// `param` is `[, cfg]`, which would otherwise leave reactive-expression
|
|
4365
|
+
// detection silently broken for destructured callbacks.
|
|
4366
|
+
// `savedScope` is restored (not deleted-from) at the matching exit site
|
|
4367
|
+
// below, so a nested loop reusing a param name can never corrupt the
|
|
4368
|
+
// outer scope.
|
|
4369
|
+
const savedScope = ctx.scope
|
|
4370
|
+
ctx.scope = ctx.scope.enterLoopRow({ param, index, paramBindings })
|
|
4336
4371
|
ctx.loopDepth++
|
|
4337
4372
|
|
|
4338
4373
|
// Logical control flow (`cond && <X/>`, `a ?? themeLogo()`) as the map
|
|
@@ -4459,6 +4494,51 @@ function transformMapCall(
|
|
|
4459
4494
|
(s): s is ts.ReturnStatement => ts.isReturnStatement(s) && s.expression != null
|
|
4460
4495
|
)
|
|
4461
4496
|
: undefined
|
|
4497
|
+
|
|
4498
|
+
// #2482 Stage 1a Commit 2 — ordering note: the STRUCTURED preamble
|
|
4499
|
+
// (`MapCallbackPreamble`, built further down by
|
|
4500
|
+
// `preambleFromValueStatements` / `buildPreambleSegments`, whichever
|
|
4501
|
+
// branch below the return-expression shape takes) isn't known until
|
|
4502
|
+
// well AFTER this point. But `transformNode(returnExpr, ctx)` just
|
|
4503
|
+
// below walks the return expression's own child expressions RIGHT
|
|
4504
|
+
// NOW — before that happens — so a preamble-declared local shadowing
|
|
4505
|
+
// an outer const/prop must already be bound in `ctx.scope`, or
|
|
4506
|
+
// `tryResolveTemplateSpanFromConst` / `tryResolveIdentifierAsTemplateLiteral`
|
|
4507
|
+
// / `rewriteBarePropRefs` bake the OUTER value into every row instead
|
|
4508
|
+
// of leaving the row-local unresolved (#2222-family, the const/prop-
|
|
4509
|
+
// shadow bug class). Fix: a lightweight declared-names-only pre-scan
|
|
4510
|
+
// — the exact same "statements before the return" shape
|
|
4511
|
+
// `preambleFromValueStatements`/`buildPreambleSegments` scan below —
|
|
4512
|
+
// re-enters the row's `BindingScope` frame (off `savedScope`, the
|
|
4513
|
+
// pre-loop parent, so this REPLACES the param/index-only frame
|
|
4514
|
+
// rather than stacking a second one) with the preamble included, for
|
|
4515
|
+
// the duration of the return-expression transform. Restored back to
|
|
4516
|
+
// the preamble-less row scope right after that window closes (see
|
|
4517
|
+
// `rowScopeBeforePreamble` below) — everything past this window
|
|
4518
|
+
// (the flatMap fallbacks, and the whole rest of `transformMapCall`
|
|
4519
|
+
// after this `if (ts.isBlock(body))` branch) must keep observing
|
|
4520
|
+
// EXACTLY the old (Commit 1) membership, since slotId-allocation
|
|
4521
|
+
// classifiers there read `ctx.scope.valueBoundNames()`, which never
|
|
4522
|
+
// included preamble names to begin with — only shadow guards need
|
|
4523
|
+
// the widened view, and only for this window.
|
|
4524
|
+
let rowScopeBeforePreamble: BindingScope | null = null
|
|
4525
|
+
if (returnStmt) {
|
|
4526
|
+
const preambleNames = new Set<string>()
|
|
4527
|
+
for (const stmt of body.statements) {
|
|
4528
|
+
if (stmt === returnStmt) break
|
|
4529
|
+
collectPreambleDeclaredNames(stmt, preambleNames)
|
|
4530
|
+
}
|
|
4531
|
+
if (preambleNames.size > 0) {
|
|
4532
|
+
rowScopeBeforePreamble = ctx.scope
|
|
4533
|
+
ctx.scope = savedScope.enterLoopRow({
|
|
4534
|
+
param,
|
|
4535
|
+
index,
|
|
4536
|
+
paramBindings,
|
|
4537
|
+
preamble: { declaredNames: [...preambleNames] },
|
|
4538
|
+
})
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4462
4542
|
if (returnStmt && returnStmt.expression) {
|
|
4463
4543
|
let returnExpr = returnStmt.expression
|
|
4464
4544
|
while (ts.isParenthesizedExpression(returnExpr)) {
|
|
@@ -4617,6 +4697,14 @@ function transformMapCall(
|
|
|
4617
4697
|
}
|
|
4618
4698
|
}
|
|
4619
4699
|
|
|
4700
|
+
// Window closed — restore the preamble-less row scope so every
|
|
4701
|
+
// consumer past this point (the flatMap fallbacks immediately
|
|
4702
|
+
// below, and the rest of `transformMapCall` after this branch)
|
|
4703
|
+
// keeps observing EXACTLY the pre-rework membership.
|
|
4704
|
+
if (rowScopeBeforePreamble) {
|
|
4705
|
+
ctx.scope = rowScopeBeforePreamble
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4620
4708
|
// flatMap block body fallback: compile JSX inline when children
|
|
4621
4709
|
// couldn't be extracted via the standard single-return path. A pure
|
|
4622
4710
|
// single-`return <call>` projection is NOT taken here — it lowers to
|
|
@@ -4697,13 +4785,10 @@ function transformMapCall(
|
|
|
4697
4785
|
)
|
|
4698
4786
|
}
|
|
4699
4787
|
|
|
4700
|
-
//
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
ctx.loopParams.delete(param)
|
|
4705
|
-
}
|
|
4706
|
-
if (index) ctx.loopParams.delete(index)
|
|
4788
|
+
// Restore the parent scope — the BindingScope twin of the old
|
|
4789
|
+
// "unregister loop params" delete block, but by reference rather than
|
|
4790
|
+
// by name, so it can never miss an entry the add-site added.
|
|
4791
|
+
ctx.scope = savedScope
|
|
4707
4792
|
ctx.loopDepth--
|
|
4708
4793
|
}
|
|
4709
4794
|
|
|
@@ -6134,10 +6219,14 @@ function tryResolveTemplateSpanFromConst(
|
|
|
6134
6219
|
// ${IDENT}
|
|
6135
6220
|
if (ts.isIdentifier(expr)) {
|
|
6136
6221
|
// #2222-family: inside a loop callback the name may be the loop's
|
|
6137
|
-
// item/index binding shadowing a same-named const —
|
|
6138
|
-
// const would bake the outer value into every row.
|
|
6139
|
-
// the bare-expression path, which sees the loop
|
|
6140
|
-
|
|
6222
|
+
// item/index/preamble binding shadowing a same-named const —
|
|
6223
|
+
// resolving the const would bake the outer value into every row.
|
|
6224
|
+
// Fall back to the bare-expression path, which sees the loop
|
|
6225
|
+
// binding. SHADOW-GUARD query — `isBound` (all sources), not
|
|
6226
|
+
// `valueBoundNames()` — see `BindingScope.valueBoundNames`'s doc
|
|
6227
|
+
// comment for the two-consumer-classes split (#2482 Stage 1a
|
|
6228
|
+
// Commit 2).
|
|
6229
|
+
if (ctx.scope.isBound(expr.text)) return null
|
|
6141
6230
|
const constInfo = findLocalConst(expr.text, ctx.analyzer)
|
|
6142
6231
|
if (!constInfo) return null
|
|
6143
6232
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -6154,7 +6243,7 @@ function tryResolveTemplateSpanFromConst(
|
|
|
6154
6243
|
// Same loop-shadowing guard as the ${IDENT} arm: `tone[k]` inside
|
|
6155
6244
|
// `items.map((tone) => …)` must read the row's `tone`, not a
|
|
6156
6245
|
// same-named module/component record const.
|
|
6157
|
-
if (ctx.
|
|
6246
|
+
if (ctx.scope.isBound(expr.expression.text)) return null
|
|
6158
6247
|
const constInfo = findLocalConst(expr.expression.text, ctx.analyzer)
|
|
6159
6248
|
if (!constInfo) return null
|
|
6160
6249
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -6326,9 +6415,13 @@ function tryResolveIdentifierAsTemplateLiteral(
|
|
|
6326
6415
|
// outer const's literal into the IR here bakes the same hard-coded
|
|
6327
6416
|
// value into EVERY adapter's output (e.g. `key={label}` inside
|
|
6328
6417
|
// `.map((label) => ...)` becoming a constant duplicate key).
|
|
6329
|
-
// `ctx.
|
|
6330
|
-
// names and index included
|
|
6331
|
-
|
|
6418
|
+
// `ctx.scope` is the live loop-binding stack (destructured binding
|
|
6419
|
+
// names and index included — plus, during the return-expression
|
|
6420
|
+
// transform window, the preamble's declared names once
|
|
6421
|
+
// `transformMapCall` re-enters the row scope with them, #2482 Stage 1a
|
|
6422
|
+
// Commit 2), so the guard is scope-accurate. This is a SHADOW-GUARD
|
|
6423
|
+
// query — reads `isBound` (all sources), not `valueBoundNames()`.
|
|
6424
|
+
if (ctx.scope.isBound(ident.text)) return null
|
|
6332
6425
|
const constInfo = findLocalConst(ident.text, ctx.analyzer)
|
|
6333
6426
|
if (!constInfo) return null
|
|
6334
6427
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -7002,10 +7095,15 @@ function isSignalOrMemoArray(array: string, ctx: TransformContext): boolean {
|
|
|
7002
7095
|
* Used by conditional transforms to assign slotId for per-item signal reactivity.
|
|
7003
7096
|
* NOT added to isReactiveExpression to avoid promoting text expressions
|
|
7004
7097
|
* like {item.name} to reactive (they use a separate slotId path).
|
|
7098
|
+
*
|
|
7099
|
+
* REACTIVITY/slotId classifier — reads `valueBoundNames()` (item/index/
|
|
7100
|
+
* destructure only), NOT preamble locals; see `BindingScope.valueBoundNames`'s
|
|
7101
|
+
* doc comment (#2482 Stage 1a Commit 2).
|
|
7005
7102
|
*/
|
|
7006
7103
|
function referencesLoopParam(expr: string, ctx: TransformContext): boolean {
|
|
7007
|
-
|
|
7008
|
-
|
|
7104
|
+
const boundNames = ctx.scope.valueBoundNames()
|
|
7105
|
+
if (boundNames.size === 0) return false
|
|
7106
|
+
for (const p of boundNames) {
|
|
7009
7107
|
if (new RegExp(`\\b${p}\\b`).test(expr)) return true
|
|
7010
7108
|
}
|
|
7011
7109
|
return false
|
|
@@ -7129,10 +7227,17 @@ function hasReactiveAttributes(attrs: IRAttribute[], ctx: TransformContext): boo
|
|
|
7129
7227
|
if (isSignalOrMemoReference(valueToCheck, ctx) || isPropsReference(valueToCheck, ctx)) {
|
|
7130
7228
|
return true
|
|
7131
7229
|
}
|
|
7132
|
-
// Check if attribute references any active loop
|
|
7133
|
-
// loop root elements need a slotId so className can be updated
|
|
7134
|
-
|
|
7135
|
-
|
|
7230
|
+
// Check if attribute references any active loop-bound VALUE names —
|
|
7231
|
+
// loop root elements need a slotId so className can be updated
|
|
7232
|
+
// reactively. REACTIVITY/slotId classifier — value bindings only
|
|
7233
|
+
// (item/index/destructure), not preamble locals; see
|
|
7234
|
+
// `BindingScope.valueBoundNames`'s doc comment (#2482 Stage 1a
|
|
7235
|
+
// Commit 2) — this is the exact check whose over-widening flipped
|
|
7236
|
+
// the `tag-cloud`/`preamble-cells` conformance fixtures before the
|
|
7237
|
+
// source-filtered query existed.
|
|
7238
|
+
const scopeValueNames = ctx.scope.valueBoundNames()
|
|
7239
|
+
if (scopeValueNames.size > 0) {
|
|
7240
|
+
for (const p of scopeValueNames) {
|
|
7136
7241
|
if (new RegExp(`\\b${p}\\b`).test(valueToCheck)) return true
|
|
7137
7242
|
}
|
|
7138
7243
|
}
|
package/src/prop-rewrite.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* stack, so `items.map((title) => title.a)` never turns into the
|
|
15
15
|
* syntactically invalid `.map((_p.title) => _p.title.a)` when `title`
|
|
16
16
|
* is also a prop. (Names bound by loop callbacks that ENCLOSE the
|
|
17
|
-
* expression are the caller's job — see the `ctx.
|
|
17
|
+
* expression are the caller's job — see the `ctx.scope` filter in
|
|
18
18
|
* `jsx-to-ir.ts`'s `rewriteBarePropRefs` wrapper, #2222.)
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BindingScope`: the one shared, immutable, stack-shaped model of "names
|
|
3
|
+
* bound by a loop callback" (#2482 Stage 0). Six independent ad-hoc
|
|
4
|
+
* mechanisms across the compiler currently answer this same question —
|
|
5
|
+
* `ctx.loopParams` (a mutated `Set<string>` in `jsx-to-ir.ts`),
|
|
6
|
+
* `collectLoopBoundNames` (`adapters/loop-bound-names.ts`),
|
|
7
|
+
* `resolveStaticLoopSource`'s `isNameShadowed` callback
|
|
8
|
+
* (`static-literal.ts`), and others — each reimplementing the same
|
|
9
|
+
* item/index/destructure/preamble-local bookkeeping with its own bugs and
|
|
10
|
+
* its own blind spots. This module is the single door those mechanisms
|
|
11
|
+
* migrate onto in later stages (Stage 0 only ships the service + tests;
|
|
12
|
+
* NO call site is migrated yet).
|
|
13
|
+
*
|
|
14
|
+
* Immutability is the point, not an incidental style choice:
|
|
15
|
+
* `ctx.loopParams` is `.add`/`.delete`-mutated as `jsx-to-ir.ts` walks in
|
|
16
|
+
* and back out of nested loops, so a caller that forgets (or races) a
|
|
17
|
+
* `.delete()` — or that holds a reference to the "current" set across a
|
|
18
|
+
* push/pop it didn't expect — silently observes the WRONG scope. A
|
|
19
|
+
* restore-bug of that shape is impossible by construction here:
|
|
20
|
+
* `enterLoopRow`/`enterCallback` never mutate `this`, they return a NEW
|
|
21
|
+
* `BindingScope` whose parent is untouched, so holding an old reference
|
|
22
|
+
* always sees the scope as it was, and there is no delete step to forget.
|
|
23
|
+
*
|
|
24
|
+
* Filter/sort callback params (`.filter(x => ...)`, `.sort((a, b) => ...)`,
|
|
25
|
+
* a nested arrow) are bound as `'callback'` frames via `enterCallback` —
|
|
26
|
+
* never folded into a `'loop-row'` frame's bindings. They are a distinct
|
|
27
|
+
* scope-introduction shape (an inner function's own parameter list, not a
|
|
28
|
+
* row's item/index/destructure/preamble names) even though both end up
|
|
29
|
+
* "just names you can't resolve against component-level state."
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How a name inside a `ScopeFrame` came to be bound — the row shapes
|
|
34
|
+
* (`'item'`/`'index'`/`'destructure'`/`'preamble'`) for `'loop-row'` frames,
|
|
35
|
+
* `'param'` for `'callback'` frames. One shared type because both frame
|
|
36
|
+
* kinds carry the same binding metadata.
|
|
37
|
+
*/
|
|
38
|
+
export type ScopeBindingSource = 'item' | 'index' | 'destructure' | 'preamble' | 'param'
|
|
39
|
+
|
|
40
|
+
export interface ScopeBinding {
|
|
41
|
+
readonly source: ScopeBindingSource
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ScopeFrame {
|
|
45
|
+
readonly kind: 'loop-row' | 'callback'
|
|
46
|
+
readonly bindings: ReadonlyMap<string, ScopeBinding>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Structural pick satisfied by BOTH `IRLoop` (`packages/jsx/src/types.ts`)
|
|
51
|
+
* and the client-JS `LoopCore` family (`packages/jsx/src/ir-to-client-js/types.ts`)
|
|
52
|
+
* without importing either — this module stays dependency-free and cannot
|
|
53
|
+
* form an import cycle with `types.ts` / `ir-to-client-js`.
|
|
54
|
+
*/
|
|
55
|
+
export interface LoopBindingSource {
|
|
56
|
+
readonly param: string
|
|
57
|
+
readonly index?: string | null
|
|
58
|
+
readonly paramBindings?: readonly { readonly name: string }[]
|
|
59
|
+
readonly preamble?: { readonly declaredNames: readonly string[] } | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A stack of `ScopeFrame`s, innermost frame at index 0 of the internal
|
|
64
|
+
* array (i.e. `frames[0]` is what `enterLoopRow`/`enterCallback` most
|
|
65
|
+
* recently pushed). `lookup`'s `depth` counts from `frames[0]`, so depth 0
|
|
66
|
+
* always means "the innermost frame," independent of how many ancestor
|
|
67
|
+
* frames exist.
|
|
68
|
+
*/
|
|
69
|
+
export class BindingScope {
|
|
70
|
+
static readonly EMPTY: BindingScope = new BindingScope([])
|
|
71
|
+
|
|
72
|
+
private constructor(private readonly frames: readonly ScopeFrame[]) {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Child scope with a new `'loop-row'` frame for one loop's per-item
|
|
76
|
+
* bindings. Parent (`this`) is not mutated; the returned scope is a
|
|
77
|
+
* NEW object with `frames = [newFrame, ...this.frames]`.
|
|
78
|
+
*
|
|
79
|
+
* Binding semantics mirror `jsx-to-ir.ts`'s `ctx.loopParams` add site
|
|
80
|
+
* EXACTLY (verified against lines ~4320-4345 and the matching delete
|
|
81
|
+
* site ~4695-4710 of `packages/jsx/src/jsx-to-ir.ts`):
|
|
82
|
+
*
|
|
83
|
+
* - When `loop.paramBindings` is non-empty (a destructured callback
|
|
84
|
+
* param, e.g. `.map(({ id, name }) => ...)`), each `paramBindings[i].name`
|
|
85
|
+
* is bound with source `'destructure'` and the raw `param` text
|
|
86
|
+
* (which for a destructured callback holds the ORIGINAL pattern
|
|
87
|
+
* source, e.g. `"{ id, name }"`, not a usable identifier) is NOT
|
|
88
|
+
* bound. This matches `jsx-to-ir.ts`:
|
|
89
|
+
* `if (paramBindings) { for (const b of paramBindings) ctx.loopParams.add(b.name) }`
|
|
90
|
+
* — the `else` branch (`ctx.loopParams.add(param)`) is skipped
|
|
91
|
+
* entirely when `paramBindings` is present.
|
|
92
|
+
* - Otherwise (a plain identifier param, e.g. `.map(item => ...)`),
|
|
93
|
+
* `param` itself is bound with source `'item'`.
|
|
94
|
+
* - `index` (the second callback param, e.g. `.map((item, i) => ...)`)
|
|
95
|
+
* is bound with source `'index'` when non-null/non-undefined.
|
|
96
|
+
* - Every name in `preamble.declaredNames` (a `.map()` callback's
|
|
97
|
+
* pre-return `const`/`let`/`function` locals, #2447) is bound with
|
|
98
|
+
* source `'preamble'`.
|
|
99
|
+
*
|
|
100
|
+
* NOTE on a sibling mechanism this method does NOT mirror:
|
|
101
|
+
* `adapters/loop-bound-names.ts`'s `collectLoopBoundNames` adds BOTH
|
|
102
|
+
* `node.param` AND every `paramBindings[i].name` unconditionally
|
|
103
|
+
* (never skipping `param` in the destructured case) — a deliberately
|
|
104
|
+
* coarser, over-inclusive collection used only to subtract names from
|
|
105
|
+
* a flat string-typing Set (safe to over-exclude there). This method
|
|
106
|
+
* follows the precise `jsx-to-ir.ts` `ctx.loopParams` semantics, since
|
|
107
|
+
* that is the mechanism actually doing scope-shadowed name RESOLUTION
|
|
108
|
+
* (the behavior `BindingScope` replaces), not coarse exclusion.
|
|
109
|
+
*/
|
|
110
|
+
enterLoopRow(loop: LoopBindingSource): BindingScope {
|
|
111
|
+
const bindings = new Map<string, ScopeBinding>()
|
|
112
|
+
if (loop.paramBindings && loop.paramBindings.length > 0) {
|
|
113
|
+
for (const b of loop.paramBindings) bindings.set(b.name, { source: 'destructure' })
|
|
114
|
+
} else {
|
|
115
|
+
bindings.set(loop.param, { source: 'item' })
|
|
116
|
+
}
|
|
117
|
+
if (loop.index != null) bindings.set(loop.index, { source: 'index' })
|
|
118
|
+
for (const name of loop.preamble?.declaredNames ?? []) bindings.set(name, { source: 'preamble' })
|
|
119
|
+
|
|
120
|
+
const frame: ScopeFrame = { kind: 'loop-row', bindings }
|
|
121
|
+
return new BindingScope([frame, ...this.frames])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Child scope with a new `'callback'` frame binding `params` (a filter
|
|
126
|
+
* predicate's `x`, a sort comparator's `(a, b)`, or a nested arrow's
|
|
127
|
+
* parameter list) with source `'param'`. Parent is not mutated.
|
|
128
|
+
*/
|
|
129
|
+
enterCallback(params: readonly string[]): BindingScope {
|
|
130
|
+
const bindings = new Map<string, ScopeBinding>()
|
|
131
|
+
for (const name of params) bindings.set(name, { source: 'param' })
|
|
132
|
+
const frame: ScopeFrame = { kind: 'callback', bindings }
|
|
133
|
+
return new BindingScope([frame, ...this.frames])
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Innermost-first membership check across every frame in the stack. */
|
|
137
|
+
isBound(name: string): boolean {
|
|
138
|
+
for (const frame of this.frames) {
|
|
139
|
+
if (frame.bindings.has(name)) return true
|
|
140
|
+
}
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves `name` against the frame stack innermost-first. `depth 0`
|
|
146
|
+
* means the innermost (most recently entered) frame; `null` when `name`
|
|
147
|
+
* is not bound in any frame.
|
|
148
|
+
*/
|
|
149
|
+
lookup(name: string): { readonly depth: number; readonly frame: ScopeFrame; readonly binding: ScopeBinding } | null {
|
|
150
|
+
for (let depth = 0; depth < this.frames.length; depth++) {
|
|
151
|
+
const frame = this.frames[depth]
|
|
152
|
+
const binding = frame.bindings.get(name)
|
|
153
|
+
if (binding) return { depth, frame, binding }
|
|
154
|
+
}
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Union of every frame's bound names (every `ScopeBindingSource`), for
|
|
160
|
+
* migration interop with legacy `Set<string>`-shaped consumers (e.g.
|
|
161
|
+
* `collectLoopBoundNames`'s return type) as later stages migrate them
|
|
162
|
+
* onto `BindingScope`.
|
|
163
|
+
*
|
|
164
|
+
* This is the SHADOW-GUARD query — see {@link valueBoundNames} for the
|
|
165
|
+
* other consumer class and why the two must not be conflated.
|
|
166
|
+
*/
|
|
167
|
+
boundNames(): ReadonlySet<string> {
|
|
168
|
+
if (this.boundNamesCache) return this.boundNamesCache
|
|
169
|
+
const names = new Set<string>()
|
|
170
|
+
for (const frame of this.frames) {
|
|
171
|
+
for (const name of frame.bindings.keys()) names.add(name)
|
|
172
|
+
}
|
|
173
|
+
this.boundNamesCache = names
|
|
174
|
+
return names
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Both name queries are hot (shadow guards, slot/reactivity classifiers,
|
|
178
|
+
// binding-env memo keying) and the scope is immutable, so each computes
|
|
179
|
+
// once per instance. Callers receive the cached set as ReadonlySet —
|
|
180
|
+
// never mutate it.
|
|
181
|
+
private boundNamesCache: ReadonlySet<string> | undefined
|
|
182
|
+
private valueBoundNamesCache: ReadonlySet<string> | undefined
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Union of names bound via `'item'`/`'index'`/`'destructure'` sources
|
|
186
|
+
* only — the loop row's own per-item identity — excluding `'preamble'`
|
|
187
|
+
* (a `.map()` callback's pre-return `const`/`let`/`function` locals,
|
|
188
|
+
* #2447) and `'param'` (an `enterCallback` frame's filter/sort/nested-
|
|
189
|
+
* arrow parameters).
|
|
190
|
+
*
|
|
191
|
+
* `BindingScope` has exactly two consumer classes, and conflating them
|
|
192
|
+
* is the #2482 Stage 1a Commit 2 regression this split exists to
|
|
193
|
+
* prevent (a `ctx.scope`-wide preamble merge flipped `tag-cloud` and
|
|
194
|
+
* `preamble-cells` conformance fixtures before this method existed):
|
|
195
|
+
*
|
|
196
|
+
* - SHADOW GUARDS (`tryResolveTemplateSpanFromConst`,
|
|
197
|
+
* `tryResolveIdentifierAsTemplateLiteral`, `rewriteBarePropRefs`
|
|
198
|
+
* in `jsx-to-ir.ts`) ask "is this name resolved to SOMETHING in
|
|
199
|
+
* this scope, so an outer const/prop of the same name must not be
|
|
200
|
+
* substituted here at this transform position" — every source
|
|
201
|
+
* qualifies, including a preamble local shadowing a module const.
|
|
202
|
+
* These call `isBound` / `boundNames()`.
|
|
203
|
+
* - REACTIVITY / SLOT-ID CLASSIFIERS (`referencesLoopParam`,
|
|
204
|
+
* `hasReactiveAttributes`, and the `BindingEnvironment.loopParams`
|
|
205
|
+
* feed built from `makeBindingEnv`, all in `jsx-to-ir.ts`) ask
|
|
206
|
+
* "does this expression read a value that changes per row and so
|
|
207
|
+
* needs its own patchable slot" — a preamble local already gets
|
|
208
|
+
* ITS OWN dedicated slot/region-patch machinery
|
|
209
|
+
* (`preambleRegions` / `markPreambleAttrSlots`, #2447), so folding
|
|
210
|
+
* it into this classification double-counts it. Worse: widening a
|
|
211
|
+
* text child's `reactive` flag this way is read by
|
|
212
|
+
* `hasDynamicContent` to decide whether the loop ROW's own root
|
|
213
|
+
* element needs a slot — an unrelated, narrower decision that must
|
|
214
|
+
* not move just because a preamble local is now scope-visible.
|
|
215
|
+
* These call `valueBoundNames()`.
|
|
216
|
+
*/
|
|
217
|
+
valueBoundNames(): ReadonlySet<string> {
|
|
218
|
+
if (this.valueBoundNamesCache) return this.valueBoundNamesCache
|
|
219
|
+
const names = new Set<string>()
|
|
220
|
+
for (const frame of this.frames) {
|
|
221
|
+
for (const [name, binding] of frame.bindings) {
|
|
222
|
+
if (binding.source === 'item' || binding.source === 'index' || binding.source === 'destructure') {
|
|
223
|
+
names.add(name)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.valueBoundNamesCache = names
|
|
228
|
+
return names
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Drop-in for `resolveStaticLoopSource`'s `opts.isNameShadowed`
|
|
233
|
+
* (`packages/jsx/src/static-literal.ts:112-128`).
|
|
234
|
+
*/
|
|
235
|
+
asShadowPredicate(): (name: string) => boolean {
|
|
236
|
+
return (name: string) => this.isBound(name)
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1882,6 +1882,14 @@ export interface ConstantInfo {
|
|
|
1882
1882
|
parsed?: ParsedExpr
|
|
1883
1883
|
/** Value with TypeScript type annotations preserved, for .tsx output */
|
|
1884
1884
|
typedValue?: string
|
|
1885
|
+
/**
|
|
1886
|
+
* The declaration's explicit type annotation, verbatim from source
|
|
1887
|
+
* (`node.type.getText()`), when the author wrote one. Distinct from
|
|
1888
|
+
* `type`, which is also populated by inference from the initializer —
|
|
1889
|
+
* emitters must only print THIS field, never an inferred type, onto a
|
|
1890
|
+
* declaration (#2589).
|
|
1891
|
+
*/
|
|
1892
|
+
typeAnnotation?: string
|
|
1885
1893
|
valueBranches?: string[]
|
|
1886
1894
|
declarationKind: 'const' | 'let'
|
|
1887
1895
|
isExported?: boolean
|