@barefootjs/jsx 0.18.4 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/dangerous-inner-html.d.ts +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +2 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1108 -307
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +30 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +19 -1
- package/src/analyzer.ts +6 -0
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +54 -1
- package/src/ir-to-client-js/html-template.ts +292 -13
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +31 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +322 -48
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
- package/src/types.ts +68 -0
package/src/jsx-to-ir.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
AttrValueOf,
|
|
35
35
|
} from './types.ts'
|
|
36
36
|
import { type AnalyzerContext, type MultiReturnJsxInfo, getSourceLocation, collectReactiveGetterNames } from './analyzer-context.ts'
|
|
37
|
-
import { parseExpression, isSupported, parseBlockBody, foldBlockToExpr, predicateTernaryToLogical, tsNodeToParsedExpr, sortComparatorFromArrow, stringifyParsedExpr, cssKebabCase, type ParsedExpr } from './expression-parser.ts'
|
|
37
|
+
import { parseExpression, isSupported, parseBlockBody, foldBlockToExpr, predicateTernaryToLogical, tsNodeToParsedExpr, sortComparatorFromArrow, stringifyParsedExpr, cssKebabCase, CALLBACK_METHODS, type ParsedExpr } from './expression-parser.ts'
|
|
38
38
|
import type { IRLoopSort, FunctionInfo } from './types.ts'
|
|
39
39
|
import { formatParamWithType } from './module-exports.ts'
|
|
40
40
|
import { createError, ErrorCodes, internalInvariant } from './errors.ts'
|
|
@@ -48,7 +48,7 @@ import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironme
|
|
|
48
48
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
49
49
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx } from './analyzer.ts'
|
|
50
50
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
51
|
-
import { toHTMLAttrName } from '@barefootjs/shared'
|
|
51
|
+
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
52
52
|
|
|
53
53
|
// =============================================================================
|
|
54
54
|
// Transform Context
|
|
@@ -82,6 +82,17 @@ interface TransformContext {
|
|
|
82
82
|
_destructuredPropNames?: Set<string> | null
|
|
83
83
|
/** Active loop parameter names for slotId assignment to loop-param-dependent expressions */
|
|
84
84
|
loopParams: Set<string>
|
|
85
|
+
/**
|
|
86
|
+
* Count of enclosing `.map()` loops (0 = outermost), incremented/
|
|
87
|
+
* decremented in lockstep with entering/leaving `transformMapCall`.
|
|
88
|
+
* Unlike `loopParams` (a name Set that can gain several entries for
|
|
89
|
+
* ONE loop level via destructuring), this is a plain per-level
|
|
90
|
+
* counter — the single source of truth `IRLoop.depth` is stamped
|
|
91
|
+
* from, so every adapter's `data-key`/`data-key-N` suffix derives
|
|
92
|
+
* from one IR-computed value instead of each adapter re-deriving
|
|
93
|
+
* nesting depth its own way (#2168 nested-loop-outer-binding).
|
|
94
|
+
*/
|
|
95
|
+
loopDepth: number
|
|
85
96
|
/** Counter for async boundary IDs (a0, a1, ...) */
|
|
86
97
|
asyncIdCounter: number
|
|
87
98
|
/** Counter for <Region> structural index (0, 1, ...) within a file. */
|
|
@@ -280,8 +291,21 @@ function exprHasFunctionCalls(expr: ts.Expression): boolean {
|
|
|
280
291
|
* Returns undefined if no rewriting is needed (SolidJS-style or no props).
|
|
281
292
|
*/
|
|
282
293
|
function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext): string | undefined {
|
|
283
|
-
|
|
294
|
+
let propNames = getDestructuredPropNames(ctx)
|
|
284
295
|
if (!propNames) return undefined
|
|
296
|
+
// #2222: a name bound as an enclosing loop callback's item/index param
|
|
297
|
+
// refers to the loop binding, not the prop, at THIS transform position —
|
|
298
|
+
// `ctx.loopParams` is the live loop-param set (destructured binding
|
|
299
|
+
// names and the index included), maintained by `transformMapCall` as it
|
|
300
|
+
// enters/leaves each callback, so this guard is scope-accurate rather
|
|
301
|
+
// than the coarse whole-component exclusion the SSR adapters use
|
|
302
|
+
// (#2221). Filter into a fresh set — `getDestructuredPropNames` caches
|
|
303
|
+
// its set on ctx and must not be mutated.
|
|
304
|
+
if (ctx.loopParams.size > 0) {
|
|
305
|
+
const filtered = new Set([...propNames].filter(n => !ctx.loopParams.has(n)))
|
|
306
|
+
if (filtered.size === 0) return undefined
|
|
307
|
+
propNames = filtered
|
|
308
|
+
}
|
|
285
309
|
// #1425: union any prop refs that `expr` reaches via branch-local
|
|
286
310
|
// text substitution. The AST walk in `rewriteBarePropRefsCore`
|
|
287
311
|
// sees only `expr`'s original AST, so a prop ref introduced via
|
|
@@ -395,6 +419,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
395
419
|
isRoot: true,
|
|
396
420
|
insideComponentChildren: false,
|
|
397
421
|
loopParams: new Set(),
|
|
422
|
+
loopDepth: 0,
|
|
398
423
|
patterns: {
|
|
399
424
|
signals: analyzer.signals.map(s => ({
|
|
400
425
|
getter: s.getter,
|
|
@@ -579,20 +604,34 @@ function parseValueExpr(trimmed: string): ParsedExpr {
|
|
|
579
604
|
return parseExpression(trimmed.startsWith('{') ? `(${trimmed})` : trimmed)
|
|
580
605
|
}
|
|
581
606
|
|
|
607
|
+
/** Shared empty-set default for `attachParsedExpressions`/`resolveCallbackMethodFunctionReferences`'s `bound` param — avoids a fresh allocation per call when nothing is shadowed. */
|
|
608
|
+
const EMPTY_BOUND: ReadonlySet<string> = new Set()
|
|
609
|
+
|
|
582
610
|
/**
|
|
583
611
|
* Attach `parsed` (`parseExpression(expr.trim())`) to every `expression` node
|
|
584
612
|
* in the tree, so SSR adapters emit from the structured tree instead of each
|
|
585
613
|
* re-parsing the string at emit time. Best-effort: a node this walk misses (or
|
|
586
614
|
* an empty `expr`) simply has no `parsed`, and the adapter falls back to
|
|
587
615
|
* parsing — so under-coverage is safe, never a behavioural change.
|
|
616
|
+
*
|
|
617
|
+
* Every parse is run through {@link resolveCallbackMethodFunctionReferences}
|
|
618
|
+
* (#2206) so a bare-identifier `.map`/`.filter`/… callback (`tags.map(format)`)
|
|
619
|
+
* resolves to its declaration wherever it can appear — text/attr/prop/provider
|
|
620
|
+
* expressions and the loop-array expression alike — not just one hardcoded
|
|
621
|
+
* spot. `bound` carries the enclosing loop(s)' `param`/`index` names down
|
|
622
|
+
* into that resolution (Fable review, #2214) so a loop item variable that
|
|
623
|
+
* happens to share a name with a module-scope const/function shadows it,
|
|
624
|
+
* same as real JS scoping — resolution stays off within that loop's body.
|
|
588
625
|
*/
|
|
589
|
-
function attachParsedExpressions(node: IRNode): void {
|
|
626
|
+
function attachParsedExpressions(node: IRNode, analyzer: AnalyzerContext, bound: ReadonlySet<string> = EMPTY_BOUND): void {
|
|
627
|
+
const parse = (trimmed: string) => resolveCallbackMethodFunctionReferences(parseExpression(trimmed), analyzer, bound)
|
|
628
|
+
const parseValue = (trimmed: string) => resolveCallbackMethodFunctionReferences(parseValueExpr(trimmed), analyzer, bound)
|
|
590
629
|
if (node.type === 'expression') {
|
|
591
630
|
const trimmed = node.expr.trim()
|
|
592
|
-
if (trimmed) node.parsed =
|
|
631
|
+
if (trimmed) node.parsed = parse(trimmed)
|
|
593
632
|
} else if (node.type === 'conditional' || node.type === 'if-statement') {
|
|
594
633
|
const trimmed = node.condition.trim()
|
|
595
|
-
if (trimmed) node.parsedCondition =
|
|
634
|
+
if (trimmed) node.parsedCondition = parse(trimmed)
|
|
596
635
|
}
|
|
597
636
|
// Attach `parsed` to every expression-valued attribute / prop so adapters can
|
|
598
637
|
// lower from the tree instead of re-parsing the string. Element attrs,
|
|
@@ -603,23 +642,23 @@ function attachParsedExpressions(node: IRNode): void {
|
|
|
603
642
|
for (const attr of node.attrs) {
|
|
604
643
|
if (attr.value.kind === 'expression') {
|
|
605
644
|
const trimmed = attr.value.expr.trim()
|
|
606
|
-
if (trimmed) attr.value.parsed =
|
|
645
|
+
if (trimmed) attr.value.parsed = parseValue(trimmed)
|
|
607
646
|
} else if (attr.value.kind === 'spread') {
|
|
608
647
|
const trimmed = attr.value.expr.trim()
|
|
609
|
-
if (trimmed) attr.value.parsed =
|
|
648
|
+
if (trimmed) attr.value.parsed = parse(trimmed)
|
|
610
649
|
}
|
|
611
650
|
}
|
|
612
651
|
} else if (node.type === 'component') {
|
|
613
652
|
for (const prop of node.props) {
|
|
614
653
|
if (prop.value.kind === 'expression') {
|
|
615
654
|
const trimmed = prop.value.expr.trim()
|
|
616
|
-
if (trimmed) prop.value.parsed =
|
|
655
|
+
if (trimmed) prop.value.parsed = parseValue(trimmed)
|
|
617
656
|
}
|
|
618
657
|
}
|
|
619
658
|
} else if (node.type === 'provider') {
|
|
620
659
|
if (node.valueProp.value.kind === 'expression') {
|
|
621
660
|
const trimmed = node.valueProp.value.expr.trim()
|
|
622
|
-
if (trimmed) node.valueProp.value.parsed =
|
|
661
|
+
if (trimmed) node.valueProp.value.parsed = parseValue(trimmed)
|
|
623
662
|
}
|
|
624
663
|
}
|
|
625
664
|
switch (node.type) {
|
|
@@ -627,46 +666,54 @@ function attachParsedExpressions(node: IRNode): void {
|
|
|
627
666
|
case 'component':
|
|
628
667
|
case 'fragment':
|
|
629
668
|
case 'provider':
|
|
630
|
-
for (const child of node.children) attachParsedExpressions(child)
|
|
669
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, bound)
|
|
631
670
|
break
|
|
632
671
|
case 'async':
|
|
633
|
-
attachParsedExpressions(node.fallback)
|
|
634
|
-
for (const child of node.children) attachParsedExpressions(child)
|
|
672
|
+
attachParsedExpressions(node.fallback, analyzer, bound)
|
|
673
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, bound)
|
|
635
674
|
break
|
|
636
675
|
case 'loop': {
|
|
637
676
|
// Attach the parse of the SAME `array` string the adapters consume
|
|
638
677
|
// (the Go adapter's scalar-literal loop typing reads `loop.array` /
|
|
639
678
|
// `nested.loopArray`, which is exactly `loop.array`), so it can read
|
|
640
|
-
// the tree instead of re-parsing with `ts.createSourceFile`.
|
|
679
|
+
// the tree instead of re-parsing with `ts.createSourceFile`. The array
|
|
680
|
+
// expression itself is evaluated in the OUTER scope (before the loop's
|
|
681
|
+
// own `param`/`index` exist), so it parses against the unextended `bound`.
|
|
641
682
|
const trimmedArray = node.array.trim()
|
|
642
|
-
if (trimmedArray) node.arrayParsed =
|
|
643
|
-
|
|
683
|
+
if (trimmedArray) node.arrayParsed = parse(trimmedArray)
|
|
684
|
+
// The loop's item/index variables shadow any same-named module-scope
|
|
685
|
+
// const/function for everything inside the loop body (Fable review,
|
|
686
|
+
// #2214) — mirrors an arrow's own params in `resolveCallbackMethodFunctionReferences`.
|
|
687
|
+
const loopBound = new Set(bound)
|
|
688
|
+
loopBound.add(node.param)
|
|
689
|
+
if (node.index) loopBound.add(node.index)
|
|
690
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, loopBound)
|
|
644
691
|
// Loops also hold expression nodes off the main `children` array.
|
|
645
692
|
if (node.childComponent) {
|
|
646
|
-
for (const child of node.childComponent.children) attachParsedExpressions(child)
|
|
693
|
+
for (const child of node.childComponent.children) attachParsedExpressions(child, analyzer, loopBound)
|
|
647
694
|
}
|
|
648
695
|
for (const nested of node.nestedComponents ?? []) {
|
|
649
|
-
for (const child of nested.children) attachParsedExpressions(child)
|
|
696
|
+
for (const child of nested.children) attachParsedExpressions(child, analyzer, loopBound)
|
|
650
697
|
}
|
|
651
698
|
for (const frag of node.flatMapCallback?.fragments ?? []) {
|
|
652
|
-
attachParsedExpressions(frag.ir)
|
|
699
|
+
attachParsedExpressions(frag.ir, analyzer, loopBound)
|
|
653
700
|
}
|
|
654
701
|
break
|
|
655
702
|
}
|
|
656
703
|
case 'conditional':
|
|
657
|
-
attachParsedExpressions(node.whenTrue)
|
|
658
|
-
attachParsedExpressions(node.whenFalse)
|
|
704
|
+
attachParsedExpressions(node.whenTrue, analyzer, bound)
|
|
705
|
+
attachParsedExpressions(node.whenFalse, analyzer, bound)
|
|
659
706
|
break
|
|
660
707
|
case 'if-statement':
|
|
661
|
-
attachParsedExpressions(node.consequent)
|
|
662
|
-
if (node.alternate) attachParsedExpressions(node.alternate)
|
|
708
|
+
attachParsedExpressions(node.consequent, analyzer, bound)
|
|
709
|
+
if (node.alternate) attachParsedExpressions(node.alternate, analyzer, bound)
|
|
663
710
|
break
|
|
664
711
|
}
|
|
665
712
|
}
|
|
666
713
|
|
|
667
714
|
export function jsxToIR(analyzer: AnalyzerContext): IRNode | null {
|
|
668
715
|
const root = buildIRRoot(analyzer)
|
|
669
|
-
if (root) attachParsedExpressions(root)
|
|
716
|
+
if (root) attachParsedExpressions(root, analyzer)
|
|
670
717
|
return root
|
|
671
718
|
}
|
|
672
719
|
|
|
@@ -1563,7 +1610,12 @@ function transformText(node: ts.JsxText, ctx: TransformContext): IRText | null {
|
|
|
1563
1610
|
|
|
1564
1611
|
return {
|
|
1565
1612
|
type: 'text',
|
|
1566
|
-
|
|
1613
|
+
// JSX decodes character references at parse time (`©` IS the
|
|
1614
|
+
// text `©`), so the IR carries the DECODED value — the semantics —
|
|
1615
|
+
// and each adapter re-escapes for its own emission context.
|
|
1616
|
+
// Decode AFTER whitespace normalization: ` ` yields U+00A0,
|
|
1617
|
+
// which `\s+` would otherwise collapse into a plain space.
|
|
1618
|
+
value: decodeEntities(text),
|
|
1567
1619
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
1568
1620
|
}
|
|
1569
1621
|
}
|
|
@@ -2483,6 +2535,31 @@ function isIteratorShapeCall(
|
|
|
2483
2535
|
return { array: node.expression.expression, shape: name }
|
|
2484
2536
|
}
|
|
2485
2537
|
|
|
2538
|
+
/**
|
|
2539
|
+
* Check if a node is the STATIC `Object.entries(x)` / `Object.keys(x)` /
|
|
2540
|
+
* `Object.values(x)` call form (#2168 object-entries-map) — the
|
|
2541
|
+
* one-argument form where `x` is a plain object/Record being iterated,
|
|
2542
|
+
* as opposed to {@link isIteratorShapeCall}'s zero-arg instance-method
|
|
2543
|
+
* form (`arr.entries()`) on an actual array. Returns the object
|
|
2544
|
+
* expression (any expression — `props.x`, `x ?? {}`, not just a bare
|
|
2545
|
+
* identifier) and the iteration shape so `transformMapCall` can strip
|
|
2546
|
+
* the `Object.<method>(...)` wrapper and record it on the IRLoop as
|
|
2547
|
+
* `objectIteration` (see that field's docstring in `types.ts` for why
|
|
2548
|
+
* this is a distinct field from `iterationShape`, not a shared one).
|
|
2549
|
+
*/
|
|
2550
|
+
function isObjectIteratorCall(
|
|
2551
|
+
node: ts.Expression,
|
|
2552
|
+
): { object: ts.Expression; shape: 'entries' | 'keys' | 'values' } | null {
|
|
2553
|
+
if (!ts.isCallExpression(node)) return null
|
|
2554
|
+
if (!ts.isPropertyAccessExpression(node.expression)) return null
|
|
2555
|
+
if (!ts.isIdentifier(node.expression.expression)) return null
|
|
2556
|
+
if (node.expression.expression.text !== 'Object') return null
|
|
2557
|
+
if (node.arguments.length !== 1) return null
|
|
2558
|
+
const name = node.expression.name.text
|
|
2559
|
+
if (name !== 'entries' && name !== 'keys' && name !== 'values') return null
|
|
2560
|
+
return { object: node.arguments[0], shape: name }
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2486
2563
|
type SortExtractionResult = {
|
|
2487
2564
|
result: IRLoopSort | null
|
|
2488
2565
|
unsupportedReason?: string
|
|
@@ -2590,8 +2667,8 @@ function extractSortComparator(
|
|
|
2590
2667
|
* vs. unresolved) is decided by the caller, not here.
|
|
2591
2668
|
*/
|
|
2592
2669
|
function resolveSortComparatorIdentifier(name: string, ctx: TransformContext): ts.Expression | null {
|
|
2593
|
-
const constInfo = findLocalConst(name, ctx)
|
|
2594
|
-
const fnInfo = findLocalFunction(name, ctx)
|
|
2670
|
+
const constInfo = findLocalConst(name, ctx.analyzer)
|
|
2671
|
+
const fnInfo = findLocalFunction(name, ctx.analyzer)
|
|
2595
2672
|
if (constInfo && fnInfo) return null
|
|
2596
2673
|
if (constInfo) {
|
|
2597
2674
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -2604,6 +2681,149 @@ function resolveSortComparatorIdentifier(name: string, ctx: TransformContext): t
|
|
|
2604
2681
|
return null
|
|
2605
2682
|
}
|
|
2606
2683
|
|
|
2684
|
+
/**
|
|
2685
|
+
* Resolve a bare-identifier callback passed to a value-position higher-order
|
|
2686
|
+
* array method (`tags.map(format)`, `.filter`, `.sort`, … — any name in
|
|
2687
|
+
* {@link CALLBACK_METHODS}, #2206) to its underlying arrow / function-
|
|
2688
|
+
* expression node — the SAME one-hop, ambiguity-refuses lookup #2090
|
|
2689
|
+
* established for `.sort(fnref)` comparators (`resolveSortComparatorIdentifier`
|
|
2690
|
+
* above). Kept as its own small function (mirroring the existing
|
|
2691
|
+
* `resolveInterleaveTagIdentifier` precedent below) rather than sharing code
|
|
2692
|
+
* with the sort resolver, so each call site's refusal semantics stay
|
|
2693
|
+
* independently reviewable.
|
|
2694
|
+
*
|
|
2695
|
+
* Only needs `AnalyzerContext` (not a full `TransformContext`) because its
|
|
2696
|
+
* caller, {@link resolveCallbackMethodFunctionReferences}, runs as a
|
|
2697
|
+
* post-parse pass over an already-built `ParsedExpr` tree — no live
|
|
2698
|
+
* `ts.Node`/source-file binding is available or needed, since
|
|
2699
|
+
* `parseConstInitializer` / `parseFunctionInfoAsExpr` re-parse the
|
|
2700
|
+
* analyzer's stored declaration TEXT into a fresh, isolated source file.
|
|
2701
|
+
*/
|
|
2702
|
+
function resolveCallbackMethodFunctionReferenceIdentifier(name: string, analyzer: AnalyzerContext): ts.Expression | null {
|
|
2703
|
+
const constInfo = findLocalConst(name, analyzer)
|
|
2704
|
+
const fnInfo = findLocalFunction(name, analyzer)
|
|
2705
|
+
if (constInfo && fnInfo) return null
|
|
2706
|
+
if (constInfo) {
|
|
2707
|
+
const ast = parseConstInitializer(constInfo)
|
|
2708
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
2709
|
+
}
|
|
2710
|
+
if (fnInfo) {
|
|
2711
|
+
const ast = parseFunctionInfoAsExpr(fnInfo)
|
|
2712
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
2713
|
+
}
|
|
2714
|
+
return null
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
/**
|
|
2718
|
+
* Post-parse pass (#2206): walk a `ParsedExpr` tree and resolve every
|
|
2719
|
+
* `<recv>.<CALLBACK_METHODS method>(<bare identifier>, …rest)` call's
|
|
2720
|
+
* callback argument to its declaration, so `tags.map(format).join(' ')`
|
|
2721
|
+
* compiles exactly as if `format`'s body had been written inline
|
|
2722
|
+
* (`tags.map(t => '#' + t).join(' ')`) — the shape `asCallbackMethodCall`
|
|
2723
|
+
* (expression-parser.ts) already recognizes and every adapter's `map_eval`
|
|
2724
|
+
* (#2073) already lowers.
|
|
2725
|
+
*
|
|
2726
|
+
* Runs AFTER `parseExpression`, not threaded into it, because the
|
|
2727
|
+
* value-returning `.map(cb)` form is recognized generically downstream of
|
|
2728
|
+
* parsing (`asCallbackMethodCall`'s `arrow.kind !== 'arrow'` check), not
|
|
2729
|
+
* during it — `convertNode` already happily produces a generic `call` node
|
|
2730
|
+
* with an `identifier` arg for `tags.map(format)` with no special-casing
|
|
2731
|
+
* needed there. An unresolvable reference (import, non-function const,
|
|
2732
|
+
* alias chain, cross-kind ambiguity — see
|
|
2733
|
+
* {@link resolveCallbackMethodFunctionReferenceIdentifier}) or a resolved
|
|
2734
|
+
* body that can't fold to a single expression is left untouched: the
|
|
2735
|
+
* `identifier` arg passes through as-is, and `asCallbackMethodCall`'s
|
|
2736
|
+
* existing arrow-kind check still refuses it with the current BF101,
|
|
2737
|
+
* unchanged.
|
|
2738
|
+
*
|
|
2739
|
+
* A spliced-in resolved arrow's OWN body is not re-walked (`visit` returns
|
|
2740
|
+
* it as-is rather than recursing) — a bare-identifier callback declared
|
|
2741
|
+
* INSIDE another resolved declaration (`const outer = xs => xs.map(inner).join(',')`
|
|
2742
|
+
* used as `rows.map(outer)`) stays unresolved and BF101-refuses, same as
|
|
2743
|
+
* before #2206. This is a deliberate one-hop limit, not an oversight: naively
|
|
2744
|
+
* recursing risks an infinite loop on a self-referential declaration
|
|
2745
|
+
* (`const f = xs => xs.map(f)`), which would need a resolution stack to
|
|
2746
|
+
* detect — out of scope here. Fails safe (refuses loudly rather than
|
|
2747
|
+
* mis-rendering); see the pinned-refusal test in map-function-reference.test.ts.
|
|
2748
|
+
*
|
|
2749
|
+
|
|
2750
|
+
* `bound` tracks names that are lexically shadowed at the current walk
|
|
2751
|
+
* position — an enclosing arrow's own params (`rows.map(fn => fn.tags.map(fn)…)`
|
|
2752
|
+
* — so a bare identifier that refers to a PARAMETER rather than a same-file
|
|
2753
|
+
* const/function is never resolved against the const/function tables
|
|
2754
|
+
* (Fable review, #2214): it grows exactly like {@link freeVarsInBody}'s own
|
|
2755
|
+
* `bound` set, at the SAME `arrow` case. The initial call from
|
|
2756
|
+
* `attachParsedExpressions` seeds it with the enclosing loop's `param`/
|
|
2757
|
+
* `index` too (a loop's item variable shadows a same-named module const the
|
|
2758
|
+
* same way a callback arrow's param does — `origin.freeRefs` already tags
|
|
2759
|
+
* such a name `kind: 'render-item'`, this walk now honors it).
|
|
2760
|
+
*/
|
|
2761
|
+
function resolveCallbackMethodFunctionReferences(
|
|
2762
|
+
expr: ParsedExpr,
|
|
2763
|
+
analyzer: AnalyzerContext,
|
|
2764
|
+
bound: ReadonlySet<string> = EMPTY_BOUND,
|
|
2765
|
+
): ParsedExpr {
|
|
2766
|
+
function visit(e: ParsedExpr, bound: ReadonlySet<string>): ParsedExpr {
|
|
2767
|
+
switch (e.kind) {
|
|
2768
|
+
case 'literal':
|
|
2769
|
+
case 'identifier':
|
|
2770
|
+
case 'regex':
|
|
2771
|
+
case 'unsupported':
|
|
2772
|
+
return e
|
|
2773
|
+
case 'call': {
|
|
2774
|
+
const callee = visit(e.callee, bound)
|
|
2775
|
+
const args = e.args.map(a => visit(a, bound))
|
|
2776
|
+
if (
|
|
2777
|
+
callee.kind === 'member' && !callee.computed &&
|
|
2778
|
+
CALLBACK_METHODS.has(callee.property) &&
|
|
2779
|
+
args[0]?.kind === 'identifier' &&
|
|
2780
|
+
!bound.has(args[0].name)
|
|
2781
|
+
) {
|
|
2782
|
+
const resolved = resolveCallbackMethodFunctionReferenceIdentifier(args[0].name, analyzer)
|
|
2783
|
+
const arrow = resolved ? tsNodeToParsedExpr(resolved) : null
|
|
2784
|
+
if (arrow && arrow.kind === 'arrow') args[0] = arrow
|
|
2785
|
+
}
|
|
2786
|
+
return { ...e, callee, args }
|
|
2787
|
+
}
|
|
2788
|
+
case 'member':
|
|
2789
|
+
return { ...e, object: visit(e.object, bound) }
|
|
2790
|
+
case 'index-access':
|
|
2791
|
+
return { ...e, object: visit(e.object, bound), index: visit(e.index, bound) }
|
|
2792
|
+
case 'binary':
|
|
2793
|
+
case 'logical':
|
|
2794
|
+
return { ...e, left: visit(e.left, bound), right: visit(e.right, bound) }
|
|
2795
|
+
case 'unary':
|
|
2796
|
+
return { ...e, argument: visit(e.argument, bound) }
|
|
2797
|
+
case 'conditional':
|
|
2798
|
+
return { ...e, test: visit(e.test, bound), consequent: visit(e.consequent, bound), alternate: visit(e.alternate, bound) }
|
|
2799
|
+
case 'template-literal':
|
|
2800
|
+
return { ...e, parts: e.parts.map(p => (p.type === 'expression' ? { ...p, expr: visit(p.expr, bound) } : p)) }
|
|
2801
|
+
case 'array-literal':
|
|
2802
|
+
return { ...e, elements: e.elements.map(el => visit(el, bound)) }
|
|
2803
|
+
case 'array-method':
|
|
2804
|
+
// `e.args` is a fixed-length tuple for some `method` variants (e.g.
|
|
2805
|
+
// `flat`'s `args: []`) — `.map(visit)` preserves length/order (a
|
|
2806
|
+
// callback resolution never adds/removes args), so this is a
|
|
2807
|
+
// same-shape cast, not a structural change. TS can't verify the
|
|
2808
|
+
// per-`method` tuple-length invariant across a `.map`, hence the
|
|
2809
|
+
// whole-object cast rather than a narrower one.
|
|
2810
|
+
return {
|
|
2811
|
+
...e,
|
|
2812
|
+
object: visit(e.object, bound),
|
|
2813
|
+
args: e.args.map(a => visit(a, bound)),
|
|
2814
|
+
...(e.method === 'flat' && e.depthExpr ? { depthExpr: visit(e.depthExpr, bound) } : {}),
|
|
2815
|
+
} as ParsedExpr
|
|
2816
|
+
case 'object-literal':
|
|
2817
|
+
return { ...e, properties: e.properties.map(p => ({ ...p, value: visit(p.value, bound) })) }
|
|
2818
|
+
case 'arrow': {
|
|
2819
|
+
const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
|
|
2820
|
+
return { ...e, body: visit(e.body, inner) }
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
return visit(expr, bound)
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2607
2827
|
/**
|
|
2608
2828
|
* Result type for extractFilterPredicate. The predicate is always an expression:
|
|
2609
2829
|
* a block body is normalized to one via `foldBlockToExpr` +
|
|
@@ -3246,6 +3466,10 @@ function transformMapCall(
|
|
|
3246
3466
|
// Capture nesting depth before we register this map's own params.
|
|
3247
3467
|
// ctx.loopParams is populated by the *outer* map; if non-empty we are inside one.
|
|
3248
3468
|
const isNested = ctx.loopParams.size > 0
|
|
3469
|
+
// This loop's own depth (0 = outermost) is however many enclosing
|
|
3470
|
+
// loops are already active, captured before `ctx.loopDepth` below is
|
|
3471
|
+
// bumped for THIS loop's own descendants.
|
|
3472
|
+
const depth = ctx.loopDepth
|
|
3249
3473
|
|
|
3250
3474
|
const propAccess = node.expression as ts.PropertyAccessExpression
|
|
3251
3475
|
const mapSource = propAccess.expression
|
|
@@ -3271,6 +3495,7 @@ function transformMapCall(
|
|
|
3271
3495
|
let templateMapPreamble: string | undefined
|
|
3272
3496
|
let typedMapPreamble: string | undefined
|
|
3273
3497
|
let iterationShape: 'entries' | 'keys' | undefined
|
|
3498
|
+
let objectIteration: 'entries' | 'keys' | 'values' | undefined
|
|
3274
3499
|
|
|
3275
3500
|
// Helper to set both array and templateArray
|
|
3276
3501
|
const setArray = (node: ts.Expression) => {
|
|
@@ -3284,8 +3509,11 @@ function transformMapCall(
|
|
|
3284
3509
|
// adapters emit the right loop variable bindings. `.values()` is a
|
|
3285
3510
|
// no-op (same as plain `.map()`) so it's stripped but not recorded.
|
|
3286
3511
|
// The inner expression (after stripping) feeds into the standard
|
|
3287
|
-
// filter/sort chain detection below.
|
|
3288
|
-
|
|
3512
|
+
// filter/sort chain detection below. Widened to `ts.Expression` (not
|
|
3513
|
+
// narrowed to `mapSource`'s own `LeftHandSideExpression` type) since
|
|
3514
|
+
// `isObjectIteratorCall`'s stripped argument can be any expression
|
|
3515
|
+
// (`x ?? {}`, not just a `LeftHandSideExpression`).
|
|
3516
|
+
let chainSource: ts.Expression = mapSource
|
|
3289
3517
|
const iteratorInfo = isIteratorShapeCall(mapSource)
|
|
3290
3518
|
if (iteratorInfo) {
|
|
3291
3519
|
chainSource = iteratorInfo.array
|
|
@@ -3295,6 +3523,18 @@ function transformMapCall(
|
|
|
3295
3523
|
iterationShape = 'keys'
|
|
3296
3524
|
}
|
|
3297
3525
|
// 'values' is a no-op — same as plain .map()
|
|
3526
|
+
} else {
|
|
3527
|
+
// Detect the STATIC `Object.entries(x)` / `.keys(x)` / `.values(x)`
|
|
3528
|
+
// form (#2168 object-entries-map) — see `isObjectIteratorCall`'s and
|
|
3529
|
+
// `IRLoop.objectIteration`'s docstrings for why this is a SEPARATE
|
|
3530
|
+
// shape from the array-instance-method case above, not a shared one.
|
|
3531
|
+
// Unlike that case, `'values'` DOES need recording here (it isn't a
|
|
3532
|
+
// no-op: `x` itself isn't iterable as a plain object).
|
|
3533
|
+
const objectIteratorInfo = isObjectIteratorCall(mapSource)
|
|
3534
|
+
if (objectIteratorInfo) {
|
|
3535
|
+
chainSource = objectIteratorInfo.object
|
|
3536
|
+
objectIteration = objectIteratorInfo.shape
|
|
3537
|
+
}
|
|
3298
3538
|
}
|
|
3299
3539
|
|
|
3300
3540
|
const filterInfo = isFilterCall(chainSource)
|
|
@@ -3407,14 +3647,19 @@ function transformMapCall(
|
|
|
3407
3647
|
setArray(innerSort.array)
|
|
3408
3648
|
}
|
|
3409
3649
|
} else {
|
|
3410
|
-
// Simple filter().map()
|
|
3411
|
-
|
|
3412
|
-
|
|
3650
|
+
// Simple filter().map(). `setArray` (not a bare `array =`
|
|
3651
|
+
// assignment) so `templateArray` carries the `_p.`-rewritten form —
|
|
3652
|
+
// the module-scope `template:` lambda can't see init's destructured
|
|
3653
|
+
// prop locals, and a bare destructured name there threw
|
|
3654
|
+
// `ReferenceError` at runtime (#2222).
|
|
3655
|
+
setArray(filterInfo.array)
|
|
3413
3656
|
}
|
|
3414
3657
|
}
|
|
3415
3658
|
} else {
|
|
3416
|
-
|
|
3417
|
-
|
|
3659
|
+
// Plain `.map()` fallback — same `setArray` requirement as above
|
|
3660
|
+
// (#2222): this is the most common loop shape and was the primary
|
|
3661
|
+
// repro for the bare-destructured-name ReferenceError.
|
|
3662
|
+
setArray(chainSource)
|
|
3418
3663
|
}
|
|
3419
3664
|
|
|
3420
3665
|
// Get callback function
|
|
@@ -3445,8 +3690,12 @@ function transformMapCall(
|
|
|
3445
3690
|
// `.entries()` synthesises `[index, value]` — when the callback
|
|
3446
3691
|
// destructures exactly two array elements, extract the names into
|
|
3447
3692
|
// `index` and `param` so the loop renders with proper bindings and
|
|
3448
|
-
// the BF104 destructure-param refusal doesn't fire.
|
|
3449
|
-
|
|
3693
|
+
// the BF104 destructure-param refusal doesn't fire. `Object.entries(x)`
|
|
3694
|
+
// (`objectIteration === 'entries'`) synthesises the SAME `[key,
|
|
3695
|
+
// value]` 2-tuple shape — `index` just holds a string key instead
|
|
3696
|
+
// of a numeric position — so it reuses this exact extraction.
|
|
3697
|
+
const isEntriesShape = iterationShape === 'entries' || objectIteration === 'entries'
|
|
3698
|
+
if (isEntriesShape && ts.isArrayBindingPattern(firstParam.name)) {
|
|
3450
3699
|
const elements = firstParam.name.elements.filter(
|
|
3451
3700
|
el => !ts.isOmittedExpression(el),
|
|
3452
3701
|
)
|
|
@@ -3485,7 +3734,7 @@ function transformMapCall(
|
|
|
3485
3734
|
}
|
|
3486
3735
|
}
|
|
3487
3736
|
}
|
|
3488
|
-
if (callback.parameters.length > 1 && iterationShape !== 'entries') {
|
|
3737
|
+
if (callback.parameters.length > 1 && iterationShape !== 'entries' && objectIteration !== 'entries') {
|
|
3489
3738
|
const secondParam = callback.parameters[1]
|
|
3490
3739
|
index = secondParam.name.getText(ctx.sourceFile)
|
|
3491
3740
|
if (secondParam.type) {
|
|
@@ -3504,6 +3753,7 @@ function transformMapCall(
|
|
|
3504
3753
|
ctx.loopParams.add(param)
|
|
3505
3754
|
}
|
|
3506
3755
|
if (index) ctx.loopParams.add(index)
|
|
3756
|
+
ctx.loopDepth++
|
|
3507
3757
|
|
|
3508
3758
|
// Logical control flow (`cond && <X/>`, `a ?? themeLogo()`) as the map
|
|
3509
3759
|
// body. This is not a JSX literal, ternary, or block, so without this
|
|
@@ -3626,6 +3876,7 @@ function transformMapCall(
|
|
|
3626
3876
|
ctx.loopParams.delete(param)
|
|
3627
3877
|
}
|
|
3628
3878
|
if (index) ctx.loopParams.delete(index)
|
|
3879
|
+
ctx.loopDepth--
|
|
3629
3880
|
}
|
|
3630
3881
|
|
|
3631
3882
|
// If no JSX children were found (e.g., callback returns a function call),
|
|
@@ -3705,6 +3956,16 @@ function transformMapCall(
|
|
|
3705
3956
|
!isSignalOrMemoArray(array, ctx)
|
|
3706
3957
|
&& !isDirectPropArray
|
|
3707
3958
|
&& !hasCalls
|
|
3959
|
+
// `objectIteration` (#2168 object-entries-map): `array` here is the
|
|
3960
|
+
// STRIPPED object expression (`Object.entries(x)`'s `x`), which can
|
|
3961
|
+
// itself be a static module-scope const object literal and would
|
|
3962
|
+
// otherwise satisfy every check above — but a plain OBJECT has no
|
|
3963
|
+
// `.forEach()`/`.map()` (the static-array client codegen's own
|
|
3964
|
+
// methods), unlike an actual array literal. Force the dynamic
|
|
3965
|
+
// `mapArray()` path instead, which this shape's client-JS array-expr
|
|
3966
|
+
// reconstruction (`applyObjectIterationWrap`, `ir-to-client-js/utils.ts`)
|
|
3967
|
+
// already handles correctly.
|
|
3968
|
+
&& !objectIteration
|
|
3708
3969
|
|
|
3709
3970
|
// Collect nested components for both static and dynamic arrays.
|
|
3710
3971
|
// Static arrays: needed for initChild hydration.
|
|
@@ -3744,6 +4005,8 @@ function transformMapCall(
|
|
|
3744
4005
|
sortComparator,
|
|
3745
4006
|
chainOrder,
|
|
3746
4007
|
iterationShape,
|
|
4008
|
+
objectIteration,
|
|
4009
|
+
depth,
|
|
3747
4010
|
clientOnly: isClientOnly || undefined,
|
|
3748
4011
|
mapPreamble,
|
|
3749
4012
|
templateMapPreamble,
|
|
@@ -4157,9 +4420,12 @@ function getAttributeValue(attr: ts.JsxAttribute, ctx: TransformContext): AttrVa
|
|
|
4157
4420
|
return AttrValueOf.booleanAttr()
|
|
4158
4421
|
}
|
|
4159
4422
|
|
|
4160
|
-
// String literal: <div id="main"
|
|
4423
|
+
// String literal: <div id="main" />. JSX decodes character references
|
|
4424
|
+
// in quoted attribute values just like in text children, so the IR
|
|
4425
|
+
// carries the decoded string (`title="Fish & Chips"` IS the value
|
|
4426
|
+
// `Fish & Chips`); adapters re-escape on emit.
|
|
4161
4427
|
if (ts.isStringLiteral(attr.initializer)) {
|
|
4162
|
-
return AttrValueOf.literal(attr.initializer.text)
|
|
4428
|
+
return AttrValueOf.literal(decodeEntities(attr.initializer.text))
|
|
4163
4429
|
}
|
|
4164
4430
|
|
|
4165
4431
|
// Expression: <div class={className} />
|
|
@@ -4351,7 +4617,7 @@ function tryResolveTemplateSpanFromConst(
|
|
|
4351
4617
|
): IRTemplatePart[] | null {
|
|
4352
4618
|
// ${IDENT}
|
|
4353
4619
|
if (ts.isIdentifier(expr)) {
|
|
4354
|
-
const constInfo = findLocalConst(expr.text, ctx)
|
|
4620
|
+
const constInfo = findLocalConst(expr.text, ctx.analyzer)
|
|
4355
4621
|
if (!constInfo) return null
|
|
4356
4622
|
const ast = parseConstInitializer(constInfo)
|
|
4357
4623
|
if (!ast) return null
|
|
@@ -4364,7 +4630,7 @@ function tryResolveTemplateSpanFromConst(
|
|
|
4364
4630
|
// ${IDENT[KEY]}
|
|
4365
4631
|
if (ts.isElementAccessExpression(expr)) {
|
|
4366
4632
|
if (!ts.isIdentifier(expr.expression)) return null
|
|
4367
|
-
const constInfo = findLocalConst(expr.expression.text, ctx)
|
|
4633
|
+
const constInfo = findLocalConst(expr.expression.text, ctx.analyzer)
|
|
4368
4634
|
if (!constInfo) return null
|
|
4369
4635
|
const ast = parseConstInitializer(constInfo)
|
|
4370
4636
|
if (!ast || !ts.isObjectLiteralExpression(ast)) return null
|
|
@@ -4421,8 +4687,8 @@ function tryResolveTemplateSpanFromConst(
|
|
|
4421
4687
|
*
|
|
4422
4688
|
* Returns undefined when no const matches.
|
|
4423
4689
|
*/
|
|
4424
|
-
function findLocalConst(name: string,
|
|
4425
|
-
const matches =
|
|
4690
|
+
function findLocalConst(name: string, analyzer: AnalyzerContext) {
|
|
4691
|
+
const matches = analyzer.localConstants.filter(c => c.name === name)
|
|
4426
4692
|
if (matches.length === 0) return undefined
|
|
4427
4693
|
// Prefer the innermost (non-module) binding; fall back to whatever's
|
|
4428
4694
|
// there if every match is at module scope.
|
|
@@ -4439,8 +4705,8 @@ function findLocalConst(name: string, ctx: TransformContext) {
|
|
|
4439
4705
|
*
|
|
4440
4706
|
* Returns undefined when no local function matches.
|
|
4441
4707
|
*/
|
|
4442
|
-
function findLocalFunction(name: string,
|
|
4443
|
-
const matches =
|
|
4708
|
+
function findLocalFunction(name: string, analyzer: AnalyzerContext) {
|
|
4709
|
+
const matches = analyzer.localFunctions.filter(f => f.name === name)
|
|
4444
4710
|
if (matches.length === 0) return undefined
|
|
4445
4711
|
const fnScoped = matches.filter(f => !f.isModule)
|
|
4446
4712
|
const pool = fnScoped.length > 0 ? fnScoped : matches
|
|
@@ -4530,7 +4796,15 @@ function tryResolveIdentifierAsTemplateLiteral(
|
|
|
4530
4796
|
ident: ts.Identifier,
|
|
4531
4797
|
ctx: TransformContext,
|
|
4532
4798
|
): IRTemplatePart[] | null {
|
|
4533
|
-
|
|
4799
|
+
// #2222/#2235: at this transform position the identifier may be an
|
|
4800
|
+
// enclosing loop callback's own (shadowing) parameter — folding the
|
|
4801
|
+
// outer const's literal into the IR here bakes the same hard-coded
|
|
4802
|
+
// value into EVERY adapter's output (e.g. `key={label}` inside
|
|
4803
|
+
// `.map((label) => ...)` becoming a constant duplicate key).
|
|
4804
|
+
// `ctx.loopParams` is the live loop-param set (destructured binding
|
|
4805
|
+
// names and index included), so the guard is scope-accurate.
|
|
4806
|
+
if (ctx.loopParams.has(ident.text)) return null
|
|
4807
|
+
const constInfo = findLocalConst(ident.text, ctx.analyzer)
|
|
4534
4808
|
if (!constInfo) return null
|
|
4535
4809
|
const ast = parseConstInitializer(constInfo)
|
|
4536
4810
|
if (!ast) return null
|
|
@@ -4740,8 +5014,8 @@ function tryDesugarInterleaveTaggedTemplate(
|
|
|
4740
5014
|
* followed, matching that precedent. Returns null when nothing resolves.
|
|
4741
5015
|
*/
|
|
4742
5016
|
function resolveInterleaveTagIdentifier(name: string, ctx: TransformContext): ts.Expression | null {
|
|
4743
|
-
const constInfo = findLocalConst(name, ctx)
|
|
4744
|
-
const fnInfo = findLocalFunction(name, ctx)
|
|
5017
|
+
const constInfo = findLocalConst(name, ctx.analyzer)
|
|
5018
|
+
const fnInfo = findLocalFunction(name, ctx.analyzer)
|
|
4745
5019
|
// A name bound BOTH as a const and as a `function` declaration is the
|
|
4746
5020
|
// same cross-kind ambiguity `resolveSortComparatorIdentifier` refuses:
|
|
4747
5021
|
// it can only occur across scopes, and `FunctionInfo.isModule` reflects
|