@barefootjs/jsx 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/format-date-lowering.d.ts +6 -3
- package/dist/format-date-lowering.d.ts.map +1 -1
- package/dist/index.js +696 -108
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +62 -23
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +47 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/format-date-lowering.test.ts +22 -6
- package/src/__tests__/reactive-factory-cross-file.test.ts +845 -0
- package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
- package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
- package/src/__tests__/to-locale-date-lowering.test.ts +203 -2
- package/src/analyzer.ts +730 -131
- package/src/errors.ts +10 -0
- package/src/format-date-lowering.ts +9 -5
- package/src/ir-to-client-js/emit-reactive.ts +14 -4
- package/src/jsx-to-ir.ts +15 -4
- package/src/to-locale-date-lowering.ts +437 -49
- package/src/types.ts +49 -3
package/src/analyzer.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
|
-
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo, DeclinedReactiveFactory, SourceLocation } from './types.ts'
|
|
10
|
+
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo, DeclinedReactiveFactory, RequiredFactoryImport, FactoryRenameSite, SourceLocation } from './types.ts'
|
|
11
11
|
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr } from './expression-parser.ts'
|
|
12
12
|
import { rewriteBarePropRefs } from './prop-rewrite.ts'
|
|
13
13
|
import { incrementCounter } from './instrumentation.ts'
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
isArrowComponentFunction,
|
|
24
24
|
collectReactiveGetterNames,
|
|
25
25
|
} from './analyzer-context.ts'
|
|
26
|
-
import { createError, createWarning, ErrorCodes } from './errors.ts'
|
|
26
|
+
import { createError, createWarning, ErrorCodes, type ErrorCode } from './errors.ts'
|
|
27
27
|
import { baseTypeName } from './rich-type-evidence.ts'
|
|
28
28
|
import { CATALOGUED_RICH_TYPE_NAMES } from './date-lowering.ts'
|
|
29
29
|
import path from 'node:path'
|
|
@@ -3945,18 +3945,158 @@ function prescanReactiveFactoriesInSource(
|
|
|
3945
3945
|
return result
|
|
3946
3946
|
}
|
|
3947
3947
|
|
|
3948
|
+
/**
|
|
3949
|
+
* #2332 — portable component-relative import specifier for a resolved
|
|
3950
|
+
* absolute helper-import target. Same `'.'`/`'./'` prefix convention as the
|
|
3951
|
+
* CLI's buildRelativeImportRewriter (packages/cli/src/lib/build.ts); strips
|
|
3952
|
+
* the resolved extension to match the codebase's extensionless-import
|
|
3953
|
+
* style. Unlike `buildRelativeImportRewriter`, this normalizes
|
|
3954
|
+
* `path.relative`'s separators to POSIX (`/`) — a backslash-separated
|
|
3955
|
+
* specifier is not valid ESM syntax, so on win32 `path.relative`'s native
|
|
3956
|
+
* output would inject a broken import rather than merely an unconventional
|
|
3957
|
+
* one (Copilot review, PR #2338).
|
|
3958
|
+
*/
|
|
3959
|
+
function toComponentRelativeSpecifier(resolvedAbs: string, componentFilePath: string): string {
|
|
3960
|
+
let rel = path.relative(path.dirname(componentFilePath), resolvedAbs).split(path.sep).join('/')
|
|
3961
|
+
rel = rel.replace(/\.(tsx|ts|jsx|js)$/, '')
|
|
3962
|
+
if (rel === '') rel = '.'
|
|
3963
|
+
if (!rel.startsWith('.')) rel = './' + rel
|
|
3964
|
+
return rel
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
/**
|
|
3968
|
+
* localName -> identity of what the entry file's own top-level named value
|
|
3969
|
+
* imports bind, for the satisfied-import dedupe check (#2332): if the
|
|
3970
|
+
* component file already imports the exact binding a factory needs to
|
|
3971
|
+
* re-provision, injecting it again would be a duplicate declaration rather
|
|
3972
|
+
* than a shadow, so that case is skipped instead of injected. `targetKey` is
|
|
3973
|
+
* the resolved absolute path for relative sources (or `unresolved:<source>`
|
|
3974
|
+
* when probing fails) and the raw specifier for bare sources.
|
|
3975
|
+
*/
|
|
3976
|
+
function buildEntryImportIndex(
|
|
3977
|
+
sf: ts.SourceFile,
|
|
3978
|
+
filePath: string
|
|
3979
|
+
): Map<string, { targetKey: string; exportedName: string }> {
|
|
3980
|
+
const index = new Map<string, { targetKey: string; exportedName: string }>()
|
|
3981
|
+
for (const stmt of sf.statements) {
|
|
3982
|
+
if (!ts.isImportDeclaration(stmt)) continue
|
|
3983
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
3984
|
+
if (stmt.importClause?.isTypeOnly) continue
|
|
3985
|
+
const src = stmt.moduleSpecifier.text
|
|
3986
|
+
const targetKey = src.startsWith('./') || src.startsWith('../')
|
|
3987
|
+
? (resolveRelativeImportToFile(src, filePath) ?? 'unresolved:' + src)
|
|
3988
|
+
: src
|
|
3989
|
+
const namedBindings = stmt.importClause?.namedBindings
|
|
3990
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
3991
|
+
for (const el of namedBindings.elements) {
|
|
3992
|
+
if (el.isTypeOnly) continue
|
|
3993
|
+
index.set(el.name.text, { targetKey, exportedName: (el.propertyName ?? el.name).text })
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
return index
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
/**
|
|
4001
|
+
* Every value-binding name anywhere in the entry file (imports, variable
|
|
4002
|
+
* declarations at any depth, function/class/enum names, function
|
|
4003
|
+
* parameters) — used by the #2332 re-provisioned-import collision check
|
|
4004
|
+
* (BF113). Deliberately over-broad: the inlined factory body lands INSIDE a
|
|
4005
|
+
* component function, where any nested binding (params, destructures,
|
|
4006
|
+
* locals) would silently shadow a top-level import injected by this round.
|
|
4007
|
+
* A scan limited to top-level bindings would miss that, so any hit anywhere
|
|
4008
|
+
* in the file declines re-provisioning with a loud BF113 rather than
|
|
4009
|
+
* risking a silent shadow — matching `moduleCaptureCheck`'s stated failure-
|
|
4010
|
+
* direction philosophy (loud build error over silent runtime break).
|
|
4011
|
+
*/
|
|
4012
|
+
function collectEntryBindingNames(sf: ts.SourceFile): Set<string> {
|
|
4013
|
+
const names = new Set<string>()
|
|
4014
|
+
function visit(node: ts.Node): void {
|
|
4015
|
+
if (ts.isImportDeclaration(node) && node.importClause) {
|
|
4016
|
+
if (node.importClause.name) names.add(node.importClause.name.text)
|
|
4017
|
+
const namedBindings = node.importClause.namedBindings
|
|
4018
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
4019
|
+
// Type-only specifiers still occupy the identifier at the TS level.
|
|
4020
|
+
for (const el of namedBindings.elements) names.add(el.name.text)
|
|
4021
|
+
}
|
|
4022
|
+
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
4023
|
+
names.add(namedBindings.name.text)
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
if (ts.isVariableDeclaration(node)) {
|
|
4027
|
+
const out: string[] = []
|
|
4028
|
+
addBindingNames(node.name, out)
|
|
4029
|
+
for (const n of out) names.add(n)
|
|
4030
|
+
}
|
|
4031
|
+
if (
|
|
4032
|
+
(ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) &&
|
|
4033
|
+
node.name
|
|
4034
|
+
) {
|
|
4035
|
+
names.add(node.name.text)
|
|
4036
|
+
}
|
|
4037
|
+
if (ts.isFunctionLike(node)) {
|
|
4038
|
+
for (const p of node.parameters) {
|
|
4039
|
+
const out: string[] = []
|
|
4040
|
+
addBindingNames(p.name, out)
|
|
4041
|
+
for (const n of out) names.add(n)
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
ts.forEachChild(node, visit)
|
|
4045
|
+
}
|
|
4046
|
+
visit(sf)
|
|
4047
|
+
return names
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
// One `export ... from` hop is followed when resolving a re-exported name
|
|
4051
|
+
// (#2341 BUG-2) — a visited-set guards against cycles regardless, so
|
|
4052
|
+
// raising this later is safe without further changes.
|
|
4053
|
+
const MAX_REEXPORT_HOPS = 1
|
|
4054
|
+
|
|
4055
|
+
/** A helper file's export surface, cached per absolute path for the
|
|
4056
|
+
* lifetime of one `prescanImportedReactiveFactories` call (#2341 BUG-2). */
|
|
4057
|
+
interface HelperFileInfo {
|
|
4058
|
+
sf: ts.SourceFile
|
|
4059
|
+
/** Module-scope function declarations exported under their external name
|
|
4060
|
+
* (own `export function`, or a local `export { f as g }`). */
|
|
4061
|
+
exportedFns: Map<string, ts.FunctionDeclaration>
|
|
4062
|
+
/** `export { a as b } from 'src'` re-exports, keyed by the EXTERNAL name
|
|
4063
|
+
* `b` — a barrel file's whole reason for existing (#2341 BUG-2). */
|
|
4064
|
+
reexports: Map<string, { source: string; innerName: string }>
|
|
4065
|
+
/** Any `export * from '...'` in this file — a named lookup that misses
|
|
4066
|
+
* `exportedFns`/`reexports` might still resolve through one of these, so
|
|
4067
|
+
* it can never be classified `'clean'`. (`export * as ns from` is a
|
|
4068
|
+
* `NamespaceExport` clause and is excluded: a named lookup can never
|
|
4069
|
+
* come through it.) */
|
|
4070
|
+
hasStarReexport: boolean
|
|
4071
|
+
moduleBindings: HelperModuleBindings
|
|
4072
|
+
}
|
|
4073
|
+
/** `'clean'` = read, parsed (or gate-skipped), and proven to define no
|
|
4074
|
+
* reactive factory under any name reachable from it. `null` = unreadable. */
|
|
4075
|
+
type LoadedHelper = HelperFileInfo | 'clean' | null
|
|
4076
|
+
type ExportLookup =
|
|
4077
|
+
| { kind: 'fn'; fn: ts.FunctionDeclaration; file: HelperFileInfo; definingPath: string }
|
|
4078
|
+
| { kind: 'clean' } // proven non-reactive under this name → cleanFactoryImports
|
|
4079
|
+
| { kind: 'unknown' } // cannot prove → leave unclassified so the BF110 name heuristic still fires
|
|
4080
|
+
|
|
3948
4081
|
/**
|
|
3949
4082
|
* Cross-file half of the factory prescan (#2325 round 2): resolve factories
|
|
3950
4083
|
* defined in a relative-imported helper file so `const { count } =
|
|
3951
4084
|
* createCounter(0)` inlines the same way whether `createCounter` lives in
|
|
3952
|
-
* this file or in `./hooks`.
|
|
4085
|
+
* this file or in `./hooks`. Follows one `export ... from` hop so a barrel
|
|
4086
|
+
* `index.ts` re-exporting the real helper resolves to the file that
|
|
4087
|
+
* actually DEFINES it (#2341 BUG-2) — every classification (factory /
|
|
4088
|
+
* declined / reactive-shaped / clean) and every downstream anchor (module-
|
|
4089
|
+
* capture check, helper-import re-provisioning, `sourceFilePath`) is keyed
|
|
4090
|
+
* off that defining file, never the barrel. Mutates `result`'s maps in
|
|
4091
|
+
* place.
|
|
3953
4092
|
*
|
|
3954
4093
|
* Perf: gated on a candidate-callee set collected from the ALREADY-parsed
|
|
3955
4094
|
* entry AST (no regex over source text, per CONTRIBUTING.md's "never parse
|
|
3956
4095
|
* imports with regex" rule) — files with no tuple/object-destructured call
|
|
3957
4096
|
* at all skip every filesystem access below. A second cheap gate (does the
|
|
3958
|
-
* helper file's raw text contain any `REACTIVE_PRIMITIVES` substring
|
|
3959
|
-
*
|
|
4097
|
+
* helper file's raw text contain any `REACTIVE_PRIMITIVES` substring, or
|
|
4098
|
+
* any `export ... from` re-export text) skips the AST parse of helper files
|
|
4099
|
+
* that plainly define no factory and re-export nothing; this is a
|
|
3960
4100
|
* skip-gate over content, not an import parse, so it doesn't run afoul of
|
|
3961
4101
|
* that same rule.
|
|
3962
4102
|
*
|
|
@@ -4020,45 +4160,49 @@ function prescanImportedReactiveFactories(
|
|
|
4020
4160
|
}
|
|
4021
4161
|
if (importsToCheck.length === 0) return
|
|
4022
4162
|
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4163
|
+
// #2332 — computed once per entry file, shared across all helper files.
|
|
4164
|
+
const entryBindingNames = collectEntryBindingNames(entrySourceFile)
|
|
4165
|
+
const entryImportIndex = buildEntryImportIndex(entrySourceFile, filePath)
|
|
4166
|
+
// localName -> planned injection identity; a later factory requiring the
|
|
4167
|
+
// same name from a DIFFERENT (targetKey, exportedName) is a collision.
|
|
4168
|
+
const plannedInjections = new Map<string, { targetKey: string; exportedName: string }>()
|
|
4169
|
+
|
|
4170
|
+
// #2341 BUG-2 — one read+parse per helper file per entry file, memoized
|
|
4171
|
+
// across every spec/hop that touches it (a barrel is typically visited
|
|
4172
|
+
// once per re-exported name it satisfies).
|
|
4173
|
+
const helperCache = new Map<string, LoadedHelper>()
|
|
4174
|
+
|
|
4175
|
+
function loadHelperFile(abs: string): LoadedHelper {
|
|
4176
|
+
const cached = helperCache.get(abs)
|
|
4177
|
+
if (cached !== undefined) return cached
|
|
4028
4178
|
|
|
4029
4179
|
let content: string
|
|
4030
4180
|
try {
|
|
4031
|
-
content = fs.readFileSync(
|
|
4181
|
+
content = fs.readFileSync(abs, 'utf8')
|
|
4032
4182
|
} catch {
|
|
4033
|
-
|
|
4183
|
+
helperCache.set(abs, null)
|
|
4184
|
+
return null
|
|
4034
4185
|
}
|
|
4035
4186
|
|
|
4036
|
-
const alreadyKnown = (name: string): boolean =>
|
|
4037
|
-
result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name)
|
|
4038
|
-
|
|
4039
4187
|
// Cheap text-level skip-gate (not an import/JS parse — see docstring):
|
|
4040
|
-
// a
|
|
4041
|
-
// define a reactive factory
|
|
4188
|
+
// a file with no reactive-primitive substring AND no re-export gate
|
|
4189
|
+
// text (`export` ... `from`) can neither define a reactive factory nor
|
|
4190
|
+
// re-export one, so skip parsing it entirely. Substring checks only —
|
|
4191
|
+
// false positives merely cause an AST parse whose outcome is still
|
|
4192
|
+
// correct, they never cause a false 'clean'.
|
|
4042
4193
|
const hasAnyPrimitiveText = [...REACTIVE_PRIMITIVES].some(p => content.includes(p))
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
continue
|
|
4194
|
+
const hasReexportText = content.includes('export') && content.includes('from')
|
|
4195
|
+
if (!hasAnyPrimitiveText && !hasReexportText) {
|
|
4196
|
+
helperCache.set(abs, 'clean')
|
|
4197
|
+
return 'clean'
|
|
4048
4198
|
}
|
|
4049
4199
|
|
|
4050
|
-
const
|
|
4051
|
-
resolved + '.prescan',
|
|
4052
|
-
content,
|
|
4053
|
-
ts.ScriptTarget.Latest,
|
|
4054
|
-
true,
|
|
4055
|
-
ts.ScriptKind.TSX
|
|
4056
|
-
)
|
|
4200
|
+
const sf = ts.createSourceFile(abs + '.prescan', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
4057
4201
|
|
|
4058
4202
|
// Map exported name -> module-scope FunctionDeclaration via the AST.
|
|
4059
4203
|
const localFns = new Map<string, ts.FunctionDeclaration>()
|
|
4060
4204
|
const exportedFns = new Map<string, ts.FunctionDeclaration>()
|
|
4061
|
-
for (const stmt of
|
|
4205
|
+
for (const stmt of sf.statements) {
|
|
4062
4206
|
if (ts.isFunctionDeclaration(stmt) && stmt.name && stmt.body) {
|
|
4063
4207
|
localFns.set(stmt.name.text, stmt)
|
|
4064
4208
|
const hasExportModifier = stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
@@ -4068,20 +4212,37 @@ function prescanImportedReactiveFactories(
|
|
|
4068
4212
|
}
|
|
4069
4213
|
}
|
|
4070
4214
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4215
|
+
|
|
4216
|
+
const reexports = new Map<string, { source: string; innerName: string }>()
|
|
4217
|
+
let hasStarReexport = false
|
|
4218
|
+
for (const stmt of sf.statements) {
|
|
4219
|
+
if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue
|
|
4220
|
+
if (!stmt.moduleSpecifier) {
|
|
4221
|
+
// `export { f }` / `export { f as g }` — keyed by the EXTERNAL export name.
|
|
4222
|
+
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
4223
|
+
for (const el of stmt.exportClause.elements) {
|
|
4224
|
+
if (el.isTypeOnly) continue
|
|
4225
|
+
const fn = localFns.get((el.propertyName ?? el.name).text)
|
|
4226
|
+
if (fn) exportedFns.set(el.name.text, fn)
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
continue
|
|
4230
|
+
}
|
|
4231
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
4232
|
+
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
4233
|
+
// `export { a as b } from 'src'` — keyed by the EXTERNAL name `b`.
|
|
4080
4234
|
for (const el of stmt.exportClause.elements) {
|
|
4081
4235
|
if (el.isTypeOnly) continue
|
|
4082
|
-
|
|
4083
|
-
|
|
4236
|
+
reexports.set(el.name.text, {
|
|
4237
|
+
source: stmt.moduleSpecifier.text,
|
|
4238
|
+
innerName: (el.propertyName ?? el.name).text,
|
|
4239
|
+
})
|
|
4084
4240
|
}
|
|
4241
|
+
} else if (!stmt.exportClause) {
|
|
4242
|
+
// `export * from 'src'`. (`export * as ns from` carries a
|
|
4243
|
+
// NamespaceExport clause here, not `undefined` — excluded on
|
|
4244
|
+
// purpose: a named lookup can never come through it.)
|
|
4245
|
+
hasStarReexport = true
|
|
4085
4246
|
}
|
|
4086
4247
|
}
|
|
4087
4248
|
|
|
@@ -4089,17 +4250,77 @@ function prescanImportedReactiveFactories(
|
|
|
4089
4250
|
// body must not reference any of these (#2325 §4h / BF112), since
|
|
4090
4251
|
// inlining moves the body into the component file where they don't
|
|
4091
4252
|
// exist.
|
|
4092
|
-
const moduleBindings = collectHelperModuleValueBindings(
|
|
4253
|
+
const moduleBindings = collectHelperModuleValueBindings(sf)
|
|
4254
|
+
|
|
4255
|
+
const info: HelperFileInfo = { sf, exportedFns, reexports, hasStarReexport, moduleBindings }
|
|
4256
|
+
helperCache.set(abs, info)
|
|
4257
|
+
return info
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
/**
|
|
4261
|
+
* Resolve `exportedName` from the file at `abs`, following one
|
|
4262
|
+
* `export ... from` hop through a barrel re-export (#2341 BUG-2).
|
|
4263
|
+
* `visited` guards against self/indirect barrel cycles.
|
|
4264
|
+
*/
|
|
4265
|
+
function lookupExportedFactory(
|
|
4266
|
+
abs: string,
|
|
4267
|
+
exportedName: string,
|
|
4268
|
+
visited: Set<string>,
|
|
4269
|
+
hopsLeft: number
|
|
4270
|
+
): ExportLookup {
|
|
4271
|
+
if (visited.has(abs)) return { kind: 'unknown' }
|
|
4272
|
+
visited.add(abs)
|
|
4273
|
+
|
|
4274
|
+
const file = loadHelperFile(abs)
|
|
4275
|
+
if (file === null) return { kind: 'unknown' }
|
|
4276
|
+
if (file === 'clean') return { kind: 'clean' }
|
|
4277
|
+
|
|
4278
|
+
const fn = file.exportedFns.get(exportedName)
|
|
4279
|
+
if (fn) return { kind: 'fn', fn, file, definingPath: abs }
|
|
4280
|
+
|
|
4281
|
+
const re = file.reexports.get(exportedName)
|
|
4282
|
+
if (re) {
|
|
4283
|
+
if (hopsLeft <= 0) return { kind: 'unknown' }
|
|
4284
|
+
// Non-relative re-export specifiers (`export { x } from 'pkg'`)
|
|
4285
|
+
// resolve through bundler/tsconfig-paths configuration this layer
|
|
4286
|
+
// doesn't consume — same restriction as direct imports.
|
|
4287
|
+
if (!re.source.startsWith('./') && !re.source.startsWith('../')) return { kind: 'unknown' }
|
|
4288
|
+
const target = resolveRelativeImportToFile(re.source, abs)
|
|
4289
|
+
if (!target) return { kind: 'unknown' } // unresolvable re-export target: never mark clean
|
|
4290
|
+
return lookupExportedFactory(target, re.innerName, visited, hopsLeft - 1)
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
// `export * from` might still reach this name through a file this
|
|
4294
|
+
// layer doesn't enumerate — never clean. Otherwise the file was fully
|
|
4295
|
+
// inspected and genuinely doesn't define or re-export this name.
|
|
4296
|
+
if (file.hasStarReexport) return { kind: 'unknown' }
|
|
4297
|
+
return { kind: 'clean' }
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
for (const { src, specs } of importsToCheck) {
|
|
4301
|
+
const resolved = resolveRelativeImportToFile(src, filePath)
|
|
4302
|
+
// Unresolvable — left alone here; the name-heuristic BF110 branch in
|
|
4303
|
+
// validateReactiveFactoryCalls handles it at validation time.
|
|
4304
|
+
if (!resolved) continue
|
|
4305
|
+
|
|
4306
|
+
const alreadyKnown = (name: string): boolean =>
|
|
4307
|
+
result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name)
|
|
4093
4308
|
|
|
4094
4309
|
for (const spec of specs) {
|
|
4095
4310
|
if (alreadyKnown(spec.local)) continue
|
|
4096
4311
|
|
|
4097
|
-
const
|
|
4098
|
-
if (
|
|
4312
|
+
const found = lookupExportedFactory(resolved, spec.exported, new Set<string>(), MAX_REEXPORT_HOPS)
|
|
4313
|
+
if (found.kind === 'clean') {
|
|
4099
4314
|
result.cleanFactoryImports.add(spec.local)
|
|
4100
4315
|
continue
|
|
4101
4316
|
}
|
|
4102
|
-
|
|
4317
|
+
if (found.kind === 'unknown') continue
|
|
4318
|
+
|
|
4319
|
+
const { fn, file, definingPath } = found
|
|
4320
|
+
const helperSf = file.sf
|
|
4321
|
+
const moduleBindings = file.moduleBindings
|
|
4322
|
+
|
|
4323
|
+
const det = detectReactiveFactory(fn, helperSf, definingPath)
|
|
4103
4324
|
if (!det) {
|
|
4104
4325
|
result.cleanFactoryImports.add(spec.local)
|
|
4105
4326
|
continue
|
|
@@ -4112,17 +4333,88 @@ function prescanImportedReactiveFactories(
|
|
|
4112
4333
|
result.declined.set(spec.local, det.declined)
|
|
4113
4334
|
break
|
|
4114
4335
|
case 'factory': {
|
|
4115
|
-
|
|
4116
|
-
|
|
4336
|
+
// Module-capture check is anchored to the DEFINING file's own
|
|
4337
|
+
// module bindings, not the barrel's (#2341 BUG-2) — the barrel
|
|
4338
|
+
// itself contributes no bindings the inlined body could reference.
|
|
4339
|
+
const capture = moduleCaptureCheck(fn, det.info, moduleBindings, fn.name!.text)
|
|
4340
|
+
if (capture.captured.length > 0) {
|
|
4117
4341
|
result.declined.set(spec.local, {
|
|
4118
4342
|
code: 'BF112',
|
|
4119
|
-
detail: `'${
|
|
4343
|
+
detail: `'${capture.captured.join("', '")}'`,
|
|
4120
4344
|
loc: det.info.loc,
|
|
4121
4345
|
})
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4346
|
+
break
|
|
4347
|
+
}
|
|
4348
|
+
// #2332 — re-provision the helper file's own named value imports
|
|
4349
|
+
// that the factory body references, instead of declining. Each
|
|
4350
|
+
// ref resolves to a component-relative specifier (or passes
|
|
4351
|
+
// through unchanged for bare/npm specifiers); a ref already
|
|
4352
|
+
// satisfied by an identical top-level import in the component
|
|
4353
|
+
// file is dropped rather than injected (would redeclare it). A
|
|
4354
|
+
// ref whose local name collides with a DIFFERENT existing/planned
|
|
4355
|
+
// binding declines with BF113 — `pending` is only merged into
|
|
4356
|
+
// `plannedInjections` on full factory success (§3.4), so a
|
|
4357
|
+
// factory that declines mid-loop reserves nothing.
|
|
4358
|
+
const required: RequiredFactoryImport[] = []
|
|
4359
|
+
const pending: Array<[string, { targetKey: string; exportedName: string }]> = []
|
|
4360
|
+
let declinedEntry: DeclinedReactiveFactory | null = null
|
|
4361
|
+
for (const ref of capture.importedRefs) {
|
|
4362
|
+
let specifier: string
|
|
4363
|
+
let targetKey: string
|
|
4364
|
+
if (ref.source.startsWith('./') || ref.source.startsWith('../')) {
|
|
4365
|
+
// Resolve from the DEFINING file's directory (#2341 BUG-2 —
|
|
4366
|
+
// `definingPath` is the file that actually declares this
|
|
4367
|
+
// import, which may differ from the barrel that was
|
|
4368
|
+
// imported). Unresolvable → same posture as a local
|
|
4369
|
+
// capture: nothing importable to re-provision (BF112).
|
|
4370
|
+
const abs = resolveRelativeImportToFile(ref.source, definingPath)
|
|
4371
|
+
if (!abs) {
|
|
4372
|
+
declinedEntry = {
|
|
4373
|
+
code: 'BF112',
|
|
4374
|
+
detail: `'${ref.localName}' (import '${ref.source}' did not resolve from the helper file)`,
|
|
4375
|
+
loc: det.info.loc,
|
|
4376
|
+
}
|
|
4377
|
+
break
|
|
4378
|
+
}
|
|
4379
|
+
specifier = toComponentRelativeSpecifier(abs, filePath)
|
|
4380
|
+
targetKey = abs
|
|
4381
|
+
} else {
|
|
4382
|
+
specifier = ref.source // bare/npm specifier — unchanged (#2332 test 2)
|
|
4383
|
+
targetKey = ref.source
|
|
4384
|
+
}
|
|
4385
|
+
// Already satisfied by an identical top-level import in the
|
|
4386
|
+
// component file — injecting again would redeclare the binding.
|
|
4387
|
+
const existing = entryImportIndex.get(ref.localName)
|
|
4388
|
+
if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName) {
|
|
4389
|
+
continue
|
|
4390
|
+
}
|
|
4391
|
+
const planned = plannedInjections.get(ref.localName)
|
|
4392
|
+
const collides =
|
|
4393
|
+
(existing !== undefined) ||
|
|
4394
|
+
(planned !== undefined && (planned.targetKey !== targetKey || planned.exportedName !== ref.exportedName)) ||
|
|
4395
|
+
(planned === undefined && entryBindingNames.has(ref.localName))
|
|
4396
|
+
if (collides) {
|
|
4397
|
+
declinedEntry = {
|
|
4398
|
+
code: 'BF113',
|
|
4399
|
+
detail: `'${ref.localName}' from '${specifier}'`,
|
|
4400
|
+
loc: det.info.loc,
|
|
4401
|
+
}
|
|
4402
|
+
break
|
|
4403
|
+
}
|
|
4404
|
+
pending.push([ref.localName, { targetKey, exportedName: ref.exportedName }])
|
|
4405
|
+
required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier })
|
|
4406
|
+
}
|
|
4407
|
+
if (declinedEntry) {
|
|
4408
|
+
result.declined.set(spec.local, declinedEntry)
|
|
4409
|
+
break
|
|
4125
4410
|
}
|
|
4411
|
+
for (const [name, id] of pending) plannedInjections.set(name, id)
|
|
4412
|
+
// #2341 BUG-2 — anchored to the file that actually defines the
|
|
4413
|
+
// factory, not the (possibly barrel) import path the component
|
|
4414
|
+
// used to reach it.
|
|
4415
|
+
det.info.sourceFilePath = definingPath
|
|
4416
|
+
if (required.length > 0) det.info.requiredImports = required
|
|
4417
|
+
result.factories.set(spec.local, det.info)
|
|
4126
4418
|
break
|
|
4127
4419
|
}
|
|
4128
4420
|
}
|
|
@@ -4131,34 +4423,56 @@ function prescanImportedReactiveFactories(
|
|
|
4131
4423
|
}
|
|
4132
4424
|
|
|
4133
4425
|
/**
|
|
4134
|
-
* Value bindings at the helper file's module scope
|
|
4135
|
-
*
|
|
4136
|
-
*
|
|
4137
|
-
*
|
|
4138
|
-
*
|
|
4139
|
-
*
|
|
4140
|
-
*
|
|
4141
|
-
*
|
|
4142
|
-
*
|
|
4143
|
-
*
|
|
4144
|
-
*
|
|
4426
|
+
* Value bindings at the helper file's module scope, split by whether they
|
|
4427
|
+
* have a re-importable module of their own (#2332).
|
|
4428
|
+
*
|
|
4429
|
+
* `local` bindings — top-level const/let/var, function/class/enum names,
|
|
4430
|
+
* plus default-import and namespace-import names — have no module a
|
|
4431
|
+
* component file could re-import them from, so an inlined factory body
|
|
4432
|
+
* referencing one unconditionally declines with BF112 (#2325 §4h): moving
|
|
4433
|
+
* the body into the component file would leave a dangling reference.
|
|
4434
|
+
*
|
|
4435
|
+
* `imported` bindings — the helper file's own named value imports — CAN be
|
|
4436
|
+
* re-provisioned: the component file can import the same binding under the
|
|
4437
|
+
* same specifier (#2332). These are collected here (keyed by the helper
|
|
4438
|
+
* file's local name) but are NOT captures; `moduleCaptureCheck` below
|
|
4439
|
+
* reports them separately from `local` hits so the caller can decide
|
|
4440
|
+
* whether to re-import rather than unconditionally decline.
|
|
4441
|
+
*
|
|
4442
|
+
* EXCEPT in both cases: imports from '@barefootjs/client' /
|
|
4443
|
+
* '@barefootjs/client/runtime'. Those are re-provisioned from usage by the
|
|
4444
|
+
* client-JS emitter regardless of where the call that used them textually
|
|
4445
|
+
* came from (`resolveFinalImports` / `detectUsedImports` regex-scan the
|
|
4446
|
+
* *generated* code, not the consumer's source imports — see #2325 spec C1),
|
|
4447
|
+
* so an inlined body calling `createSignal` is never a capture even though
|
|
4448
|
+
* the helper file itself imports it. Type-only imports/declarations carry
|
|
4449
|
+
* no runtime binding and are excluded.
|
|
4145
4450
|
*/
|
|
4146
|
-
|
|
4147
|
-
|
|
4451
|
+
interface HelperModuleBindings {
|
|
4452
|
+
/** Declared directly in the helper file — unconditional BF112 capture. */
|
|
4453
|
+
local: Set<string>
|
|
4454
|
+
/** Named value-import specifiers, keyed by helper-file local name —
|
|
4455
|
+
* re-provisionable into the component file (#2332). */
|
|
4456
|
+
imported: Map<string, { source: string; exportedName: string }>
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
function collectHelperModuleValueBindings(sf: ts.SourceFile): HelperModuleBindings {
|
|
4460
|
+
const local = new Set<string>()
|
|
4461
|
+
const imported = new Map<string, { source: string; exportedName: string }>()
|
|
4148
4462
|
for (const stmt of sf.statements) {
|
|
4149
4463
|
if (ts.isVariableStatement(stmt)) {
|
|
4150
4464
|
const out: string[] = []
|
|
4151
4465
|
for (const decl of stmt.declarationList.declarations) {
|
|
4152
4466
|
addBindingNames(decl.name, out)
|
|
4153
4467
|
}
|
|
4154
|
-
for (const n of out)
|
|
4468
|
+
for (const n of out) local.add(n)
|
|
4155
4469
|
continue
|
|
4156
4470
|
}
|
|
4157
4471
|
if (
|
|
4158
4472
|
(ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt)) &&
|
|
4159
4473
|
stmt.name
|
|
4160
4474
|
) {
|
|
4161
|
-
|
|
4475
|
+
local.add(stmt.name.text)
|
|
4162
4476
|
continue
|
|
4163
4477
|
}
|
|
4164
4478
|
if (ts.isImportDeclaration(stmt)) {
|
|
@@ -4166,42 +4480,62 @@ function collectHelperModuleValueBindings(sf: ts.SourceFile): Set<string> {
|
|
|
4166
4480
|
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
4167
4481
|
const src = stmt.moduleSpecifier.text
|
|
4168
4482
|
if (src === '@barefootjs/client' || src === '@barefootjs/client/runtime') continue
|
|
4169
|
-
|
|
4483
|
+
// Default/namespace imports stay hard BF112 (#2332 scope decision):
|
|
4484
|
+
// no single named export to re-provision under one local name.
|
|
4485
|
+
if (stmt.importClause?.name) local.add(stmt.importClause.name.text)
|
|
4170
4486
|
const namedBindings = stmt.importClause?.namedBindings
|
|
4171
4487
|
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
4172
4488
|
for (const el of namedBindings.elements) {
|
|
4173
4489
|
if (el.isTypeOnly) continue
|
|
4174
|
-
|
|
4490
|
+
imported.set(el.name.text, { source: src, exportedName: (el.propertyName ?? el.name).text })
|
|
4175
4491
|
}
|
|
4176
4492
|
}
|
|
4177
4493
|
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
4178
|
-
|
|
4494
|
+
local.add(namedBindings.name.text)
|
|
4179
4495
|
}
|
|
4180
4496
|
}
|
|
4181
4497
|
}
|
|
4182
|
-
return
|
|
4498
|
+
return { local, imported }
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
/**
|
|
4502
|
+
* Categorized free-identifier references of a reactive-factory body into
|
|
4503
|
+
* its own module scope (#2332): `captured` are unconditional BF112 hits
|
|
4504
|
+
* (helper-local bindings — the body would dangle if inlined verbatim);
|
|
4505
|
+
* `importedRefs` are references to the helper file's own named value
|
|
4506
|
+
* imports, which the caller may re-provision into the component file
|
|
4507
|
+
* instead of declining (§3.4 in the #2332 spec).
|
|
4508
|
+
*/
|
|
4509
|
+
interface ModuleCaptureResult {
|
|
4510
|
+
/** Free refs resolving to helper-local bindings (BF112), sorted. */
|
|
4511
|
+
captured: string[]
|
|
4512
|
+
/** Free refs resolving to the helper's own named value imports, sorted by localName. */
|
|
4513
|
+
importedRefs: Array<{ localName: string; source: string; exportedName: string }>
|
|
4183
4514
|
}
|
|
4184
4515
|
|
|
4185
4516
|
/**
|
|
4186
4517
|
* Free identifiers of a reactive-factory body that resolve to bindings at
|
|
4187
|
-
* its own module scope (#2325 §4h / BF112) — references the inlined
|
|
4188
|
-
* would silently lose once spliced into the component file
|
|
4189
|
-
*
|
|
4518
|
+
* its own module scope (#2325 §4h / BF112, #2332) — references the inlined
|
|
4519
|
+
* body would silently lose once spliced into the component file, unless
|
|
4520
|
+
* re-provisioned as an import. Returns `captured` (unconditional BF112) and
|
|
4521
|
+
* `importedRefs` (re-provisionable) separately, each sorted for stable
|
|
4522
|
+
* diagnostic/injection text.
|
|
4190
4523
|
*
|
|
4191
4524
|
* Known accepted limitation: `extractFreeIdentifiersFromNode` only scope-
|
|
4192
4525
|
* tracks arrow-function parameters, not nested `function` declarations'
|
|
4193
4526
|
* parameters or nested-block declarations — a body-nested binding that
|
|
4194
4527
|
* happens to shadow a helper-module binding could false-positive into
|
|
4195
|
-
* BF112
|
|
4196
|
-
*
|
|
4528
|
+
* BF112 or `importedRefs`. Acceptable: the failure direction is always a
|
|
4529
|
+
* loud build error (BF112/BF113) or a redundant-but-harmless injected
|
|
4530
|
+
* import, never a silent dangling reference.
|
|
4197
4531
|
*/
|
|
4198
4532
|
function moduleCaptureCheck(
|
|
4199
4533
|
fn: ts.FunctionDeclaration,
|
|
4200
4534
|
info: ReactiveFactoryInfo,
|
|
4201
|
-
moduleBindings:
|
|
4535
|
+
moduleBindings: HelperModuleBindings,
|
|
4202
4536
|
selfName: string
|
|
4203
|
-
):
|
|
4204
|
-
if (!fn.body) return []
|
|
4537
|
+
): ModuleCaptureResult {
|
|
4538
|
+
if (!fn.body) return { captured: [], importedRefs: [] }
|
|
4205
4539
|
const free = extractFreeIdentifiersFromNode(fn.body)
|
|
4206
4540
|
const exclude = new Set<string>(info.params)
|
|
4207
4541
|
for (const b of info.localBindings) exclude.add(b)
|
|
@@ -4209,12 +4543,20 @@ function moduleCaptureCheck(
|
|
|
4209
4543
|
for (const p of REACTIVE_PRIMITIVES) exclude.add(p)
|
|
4210
4544
|
exclude.add(selfName)
|
|
4211
4545
|
|
|
4212
|
-
const
|
|
4546
|
+
const captured: string[] = []
|
|
4547
|
+
const importedRefs: ModuleCaptureResult['importedRefs'] = []
|
|
4213
4548
|
for (const id of free) {
|
|
4214
4549
|
if (exclude.has(id)) continue
|
|
4215
|
-
if (moduleBindings.has(id))
|
|
4550
|
+
if (moduleBindings.local.has(id)) {
|
|
4551
|
+
captured.push(id)
|
|
4552
|
+
continue
|
|
4553
|
+
}
|
|
4554
|
+
const imp = moduleBindings.imported.get(id)
|
|
4555
|
+
if (imp) importedRefs.push({ localName: id, source: imp.source, exportedName: imp.exportedName })
|
|
4216
4556
|
}
|
|
4217
|
-
|
|
4557
|
+
captured.sort()
|
|
4558
|
+
importedRefs.sort((a, b) => (a.localName < b.localName ? -1 : 1))
|
|
4559
|
+
return { captured, importedRefs }
|
|
4218
4560
|
}
|
|
4219
4561
|
|
|
4220
4562
|
/**
|
|
@@ -4256,6 +4598,27 @@ function detectReactiveFactory(
|
|
|
4256
4598
|
|
|
4257
4599
|
const loc = getSourceLocation(node, sourceFile, filePath)
|
|
4258
4600
|
|
|
4601
|
+
// Count `return`s ANYWHERE in the body, stopping at nested function-like
|
|
4602
|
+
// boundaries (ts.isFunctionLike — functions, arrows, methods, accessors,
|
|
4603
|
+
// and constructors; broader than isMultiReturnJsxFunctionBody's #932
|
|
4604
|
+
// three-kind check, which only needs to catch JSX-returning callbacks and
|
|
4605
|
+
// was never exercised against class/object methods) — a return inside a
|
|
4606
|
+
// nested callback OR a class/object method declared in the factory body
|
|
4607
|
+
// belongs to that inner scope, not this factory (Copilot review, PR
|
|
4608
|
+
// #2342: the narrower check would misclassify a factory with, e.g., a
|
|
4609
|
+
// helper class whose method contains a `return` as having multiple
|
|
4610
|
+
// returns, declining it unnecessarily). A return nested in if/try/loop
|
|
4611
|
+
// blocks DOES count, so guard-clause factories are declassified instead
|
|
4612
|
+
// of splicing an early `return` into the component's init function
|
|
4613
|
+
// (#2341 BUG-3).
|
|
4614
|
+
let totalReturnCount = 0
|
|
4615
|
+
function countReturns(n: ts.Node): void {
|
|
4616
|
+
if (ts.isFunctionLike(n)) return
|
|
4617
|
+
if (ts.isReturnStatement(n)) { totalReturnCount++; return }
|
|
4618
|
+
ts.forEachChild(n, countReturns)
|
|
4619
|
+
}
|
|
4620
|
+
ts.forEachChild(node.body, countReturns)
|
|
4621
|
+
|
|
4259
4622
|
// Require exactly one top-level `return`, whose argument (after unwrapping
|
|
4260
4623
|
// parens / `as const` / type-assertion) is a tuple (array literal) or a
|
|
4261
4624
|
// shorthand-object literal.
|
|
@@ -4272,7 +4635,12 @@ function detectReactiveFactory(
|
|
|
4272
4635
|
if (ts.isTypeAssertionExpression(expr)) expr = expr.expression
|
|
4273
4636
|
returnExpr = expr
|
|
4274
4637
|
}
|
|
4275
|
-
|
|
4638
|
+
// `totalReturnCount === 1 && returnCount === 1` jointly guarantee the
|
|
4639
|
+
// single return is a direct child of `node.body` (top-level returns are a
|
|
4640
|
+
// subset of total) — so a guard-clause / try-catch / loop return
|
|
4641
|
+
// declassifies the factory instead of silently producing an inert
|
|
4642
|
+
// component (#2341 BUG-3).
|
|
4643
|
+
if (totalReturnCount !== 1 || returnCount !== 1 || !returnExpr) return { kind: 'reactive-shaped' }
|
|
4276
4644
|
|
|
4277
4645
|
const returnTupleIdentifiers: string[] = []
|
|
4278
4646
|
let returnKind: 'tuple' | 'object'
|
|
@@ -4308,6 +4676,22 @@ function detectReactiveFactory(
|
|
|
4308
4676
|
return { kind: 'reactive-shaped' }
|
|
4309
4677
|
}
|
|
4310
4678
|
|
|
4679
|
+
// Collect parameter names first — the rename-site walk below needs them
|
|
4680
|
+
// as part of `relevantNames` and to detect param-shadowing declarations
|
|
4681
|
+
// (BF114). Hoisted above local-binding/body-serialization collection
|
|
4682
|
+
// (#2341 BUG-1); it has no dependency on either.
|
|
4683
|
+
const params: string[] = []
|
|
4684
|
+
for (const p of node.parameters) {
|
|
4685
|
+
if (ts.isIdentifier(p.name)) {
|
|
4686
|
+
params.push(p.name.text)
|
|
4687
|
+
continue
|
|
4688
|
+
}
|
|
4689
|
+
// Destructured params are uncommon for this helper shape and out of
|
|
4690
|
+
// initial scope; the factory still wraps a reactive primitive, so
|
|
4691
|
+
// classify it as reactive-shaped rather than silently ignoring it.
|
|
4692
|
+
return { kind: 'reactive-shaped' }
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4311
4695
|
// Collect local bindings in the factory body for identifier hygiene at
|
|
4312
4696
|
// inlining time. Only direct-child declarations of the block are
|
|
4313
4697
|
// considered — good enough for the typical helper shape.
|
|
@@ -4322,24 +4706,146 @@ function detectReactiveFactory(
|
|
|
4322
4706
|
}
|
|
4323
4707
|
}
|
|
4324
4708
|
|
|
4709
|
+
// Names that call-site inlining may rename: params (substituted with
|
|
4710
|
+
// arbitrary argument expressions), local bindings (suffix-renamed for
|
|
4711
|
+
// per-call-site hygiene), and return identifiers (renamed to the
|
|
4712
|
+
// caller's destructure names). Every other identifier in the body is
|
|
4713
|
+
// left untouched — the #2341 BUG-1 fix for the old whole-body regex
|
|
4714
|
+
// renames, which corrupted string/template literals, property keys,
|
|
4715
|
+
// and `.prop` tails that merely happened to share a relevant name.
|
|
4716
|
+
const relevantNames = new Set<string>([...params, ...localBindings, ...returnTupleIdentifiers])
|
|
4717
|
+
|
|
4718
|
+
const renameSites: FactoryRenameSite[] = []
|
|
4719
|
+
let shadowedParam: string | null = null
|
|
4720
|
+
|
|
4721
|
+
/**
|
|
4722
|
+
* Recursive AST walk collecting rename sites for one kept statement.
|
|
4723
|
+
* `toBodyOffset` converts a position in `sourceFile` (this statement's
|
|
4724
|
+
* original source) to an offset in the final joined `bodyStatements`
|
|
4725
|
+
* string — see the join loop below for why this must stay a pure
|
|
4726
|
+
* function of `stmtStart`/`base`.
|
|
4727
|
+
*/
|
|
4728
|
+
function collectRenameSites(root: ts.Node, toBodyOffset: (pos: number) => number): void {
|
|
4729
|
+
function push(id: ts.Identifier, form: 'plain' | 'shorthand'): void {
|
|
4730
|
+
renameSites.push({
|
|
4731
|
+
name: id.text,
|
|
4732
|
+
start: toBodyOffset(id.getStart(sourceFile)),
|
|
4733
|
+
end: toBodyOffset(id.getEnd()),
|
|
4734
|
+
form,
|
|
4735
|
+
})
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
function classify(id: ts.Identifier): void {
|
|
4739
|
+
if (!relevantNames.has(id.text)) return
|
|
4740
|
+
const p = id.parent
|
|
4741
|
+
// Pure-key / non-reference positions — never rename:
|
|
4742
|
+
if (ts.isPropertyAccessExpression(p) && p.name === id) return // obj.name tail (incl. ?. chains)
|
|
4743
|
+
if (ts.isPropertyAssignment(p) && p.name === id) return // { name: v } key
|
|
4744
|
+
if (ts.isBindingElement(p) && p.propertyName === id) return // { name: local } pattern key
|
|
4745
|
+
if ((ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) ||
|
|
4746
|
+
ts.isSetAccessorDeclaration(p) || ts.isPropertyDeclaration(p) ||
|
|
4747
|
+
ts.isEnumMember(p)) && p.name === id) return // member keys
|
|
4748
|
+
if (ts.isJsxAttribute(p) && p.name === id) return // JSX attr name
|
|
4749
|
+
if ((ts.isLabeledStatement(p) && p.label === id) ||
|
|
4750
|
+
((ts.isBreakStatement(p) || ts.isContinueStatement(p)) && p.label === id)) return
|
|
4751
|
+
if ((ts.isJsxOpeningElement(p) || ts.isJsxSelfClosingElement(p) || ts.isJsxClosingElement(p)) &&
|
|
4752
|
+
p.tagName === id && /^[a-z]/.test(id.text)) return // intrinsic tag <div>
|
|
4753
|
+
|
|
4754
|
+
// Shorthand dual-role positions → expansion form (renaming must
|
|
4755
|
+
// preserve the implied key): `{ name }` object literal / pattern.
|
|
4756
|
+
if (ts.isShorthandPropertyAssignment(p) && p.name === id) { push(id, 'shorthand'); return }
|
|
4757
|
+
if (ts.isBindingElement(p) && p.name === id && !p.propertyName &&
|
|
4758
|
+
ts.isObjectBindingPattern(p.parent)) {
|
|
4759
|
+
// ArrayBindingPattern trap: a tuple destructure element
|
|
4760
|
+
// (`const [a, b] = ...`) ALSO has propertyName === undefined —
|
|
4761
|
+
// the ts.isObjectBindingPattern(p.parent) check above is what
|
|
4762
|
+
// keeps tuple elements out of the shorthand-expansion path.
|
|
4763
|
+
if (params.includes(id.text)) shadowedParam = id.text // decl of a param name → BF114
|
|
4764
|
+
push(id, 'shorthand')
|
|
4765
|
+
return
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
// Declaration-name positions → plain rename, but a param name
|
|
4769
|
+
// re-declared here is a shadow: params are substituted with
|
|
4770
|
+
// arbitrary argument expressions, so a shadowing declaration would
|
|
4771
|
+
// receive an invalid left-hand side (BF114 declines instead).
|
|
4772
|
+
const isDecl =
|
|
4773
|
+
(ts.isVariableDeclaration(p) || ts.isParameter(p) || ts.isBindingElement(p) ||
|
|
4774
|
+
ts.isFunctionDeclaration(p) || ts.isFunctionExpression(p) ||
|
|
4775
|
+
ts.isClassDeclaration(p) || ts.isClassExpression(p)) && p.name === id
|
|
4776
|
+
if (isDecl && params.includes(id.text)) shadowedParam = id.text
|
|
4777
|
+
|
|
4778
|
+
push(id, 'plain') // genuine value reference or decl
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
function visit(n: ts.Node): void {
|
|
4782
|
+
// Never descend into type-land: annotations are stripped downstream,
|
|
4783
|
+
// and splicing an argument EXPRESSION into a type position would be
|
|
4784
|
+
// invalid JS.
|
|
4785
|
+
if (ts.isTypeNode(n) || ts.isTypeParameterDeclaration(n) ||
|
|
4786
|
+
ts.isTypeAliasDeclaration(n) || ts.isInterfaceDeclaration(n)) return
|
|
4787
|
+
if (ts.isIdentifier(n)) { classify(n); return } // identifiers have no relevant children
|
|
4788
|
+
ts.forEachChild(n, visit)
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
visit(root)
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4325
4794
|
// Serialize the body without the outer braces and without the return
|
|
4326
4795
|
// statement — the return tuple/object is dissolved into caller-named
|
|
4327
|
-
// identifiers.
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4796
|
+
// identifiers. `renameSites` is collected in this SAME loop so its
|
|
4797
|
+
// offsets (relative to the joined `bodyStatements` string) can never
|
|
4798
|
+
// drift from the join logic — this invariant is the single most fragile
|
|
4799
|
+
// part of the rename mechanism (#2341 BUG-1): site collection and
|
|
4800
|
+
// `bodyStatements` construction must use the identical statement filter,
|
|
4801
|
+
// identical `getText` slices, and identical '\n' join.
|
|
4802
|
+
const keptStatements = node.body.statements.filter(s => !ts.isReturnStatement(s))
|
|
4803
|
+
const pieces: string[] = []
|
|
4804
|
+
let base = 0
|
|
4805
|
+
for (const stmt of keptStatements) {
|
|
4806
|
+
const text = stmt.getText(sourceFile)
|
|
4807
|
+
const stmtStart = stmt.getStart(sourceFile)
|
|
4808
|
+
collectRenameSites(stmt, (pos) => pos - stmtStart + base)
|
|
4809
|
+
pieces.push(text)
|
|
4810
|
+
base += text.length + 1 // +1 for the '\n' join — MUST match the join below
|
|
4811
|
+
}
|
|
4812
|
+
const bodyStatements = pieces.join('\n')
|
|
4813
|
+
|
|
4814
|
+
if (shadowedParam !== null) {
|
|
4815
|
+
return {
|
|
4816
|
+
kind: 'declined',
|
|
4817
|
+
declined: {
|
|
4818
|
+
code: 'BF114',
|
|
4819
|
+
detail: `parameter '${shadowedParam}' of '${node.name.text}' is shadowed by a nested declaration inside the factory body`,
|
|
4820
|
+
loc,
|
|
4821
|
+
},
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4332
4824
|
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4825
|
+
// Dev invariant: every collected site's [start, end) range must slice
|
|
4826
|
+
// out exactly the identifier text it was collected for, or the
|
|
4827
|
+
// bottom-to-top splice in inlineFactoryCallAtSite would corrupt
|
|
4828
|
+
// unrelated text. Cheap (bounded by body size) — kept as a permanent
|
|
4829
|
+
// guard against the offset math drifting under a future edit. Declines
|
|
4830
|
+
// rather than throwing (Copilot review, PR #2342): compileJSX does not
|
|
4831
|
+
// catch analyzer errors, so a thrown exception here would crash the
|
|
4832
|
+
// whole compilation instead of failing one factory loudly-but-
|
|
4833
|
+
// gracefully as a diagnostic — this should never trigger in practice,
|
|
4834
|
+
// but the failure mode if it ever does must stay "loud decline," not
|
|
4835
|
+
// "hard crash," matching this feature's whole design philosophy.
|
|
4836
|
+
for (const site of renameSites) {
|
|
4837
|
+
if (bodyStatements.slice(site.start, site.end) !== site.name) {
|
|
4838
|
+
return {
|
|
4839
|
+
kind: 'declined',
|
|
4840
|
+
declined: {
|
|
4841
|
+
code: 'BF111',
|
|
4842
|
+
detail:
|
|
4843
|
+
`internal rename-site offset mismatch for '${site.name}' — this is a compiler bug, ` +
|
|
4844
|
+
`please report it`,
|
|
4845
|
+
loc,
|
|
4846
|
+
},
|
|
4847
|
+
}
|
|
4338
4848
|
}
|
|
4339
|
-
// Destructured params are uncommon for this helper shape and out of
|
|
4340
|
-
// initial scope; the factory still wraps a reactive primitive, so
|
|
4341
|
-
// classify it as reactive-shaped rather than silently ignoring it.
|
|
4342
|
-
return { kind: 'reactive-shaped' }
|
|
4343
4849
|
}
|
|
4344
4850
|
|
|
4345
4851
|
return {
|
|
@@ -4351,6 +4857,7 @@ function detectReactiveFactory(
|
|
|
4351
4857
|
returnKind,
|
|
4352
4858
|
localBindings,
|
|
4353
4859
|
loc,
|
|
4860
|
+
renameSites,
|
|
4354
4861
|
},
|
|
4355
4862
|
}
|
|
4356
4863
|
}
|
|
@@ -4393,6 +4900,12 @@ function rewriteFactoryCallsInSource(
|
|
|
4393
4900
|
type Edit = { start: number; end: number; replacement: string }
|
|
4394
4901
|
const edits: Edit[] = []
|
|
4395
4902
|
let callSiteIndex = 0
|
|
4903
|
+
// #2332 — factories actually inlined in this walk (not merely present in
|
|
4904
|
+
// `prescan.factories`; `maybeRewriteDecl` bails on arity mismatch/omitted
|
|
4905
|
+
// elements/rename destructures without inlining), so their
|
|
4906
|
+
// `requiredImports` can be re-provisioned without adding a dead import
|
|
4907
|
+
// for a factory that was never actually spliced in.
|
|
4908
|
+
const inlinedFactories = new Set<ReactiveFactoryInfo>()
|
|
4396
4909
|
|
|
4397
4910
|
function visitStmt(node: ts.Node, inComponent: boolean): void {
|
|
4398
4911
|
if (ts.isVariableStatement(node) && inComponent) {
|
|
@@ -4513,30 +5026,49 @@ function rewriteFactoryCallsInSource(
|
|
|
4513
5026
|
const thisCallIndex = callSiteIndex++
|
|
4514
5027
|
const suffix = `_bf${thisCallIndex}`
|
|
4515
5028
|
|
|
4516
|
-
//
|
|
4517
|
-
|
|
4518
|
-
//
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
5029
|
+
// Merged rename map, one entry per old name → replacement text.
|
|
5030
|
+
// Precedence (matches the pre-#2341 sequential-pass outcome): param
|
|
5031
|
+
// substitution > return→caller rename > internal suffix rename — later
|
|
5032
|
+
// `.set()` calls for the same key overwrite earlier ones below.
|
|
5033
|
+
const renames = new Map<string, string>()
|
|
5034
|
+
// 1. Suffix-rename internal bindings not excluded (tuple params/returns,
|
|
5035
|
+
// or object-path destructured names — see excludeFromSuffixRename's
|
|
5036
|
+
// callers).
|
|
5037
|
+
for (const name of factory.localBindings) {
|
|
5038
|
+
if (!excludeFromSuffixRename.has(name)) renames.set(name, name + suffix)
|
|
5039
|
+
}
|
|
5040
|
+
// 2. Return identifiers → caller destructure names (tuple path only).
|
|
5041
|
+
if (renameReturnToCallerNames) {
|
|
5042
|
+
for (const [n, caller] of renameReturnToCallerNames) {
|
|
5043
|
+
if (caller !== n) renames.set(n, caller) // identity rename = no-op, skip
|
|
5044
|
+
}
|
|
4523
5045
|
}
|
|
4524
|
-
//
|
|
5046
|
+
// 3. Parameters → argument expressions. Atomic arguments (bare
|
|
4525
5047
|
// identifiers, numeric literals, string literals) are spliced
|
|
4526
5048
|
// directly; anything more complex is wrapped in parens to preserve
|
|
4527
|
-
// operator precedence at the splice site.
|
|
5049
|
+
// operator precedence at the splice site. Overwrites any same-name
|
|
5050
|
+
// return rename above — param substitution wins, as before #2341.
|
|
4528
5051
|
const atomicArg = /^(?:[\w$.]+|'[^'\\]*'|"[^"\\]*"|-?\d+(?:\.\d+)?)$/
|
|
4529
5052
|
for (let i = 0; i < factory.params.length; i++) {
|
|
4530
5053
|
const p = factory.params[i]
|
|
4531
|
-
const a = argTexts[i] ?? 'undefined'
|
|
4532
|
-
|
|
4533
|
-
body = body.replace(new RegExp(`\\b${escapeRegex(p)}\\b`, 'g'), wrapped)
|
|
5054
|
+
const a = (argTexts[i] ?? 'undefined').trim()
|
|
5055
|
+
renames.set(p, atomicArg.test(a) ? a : `(${a})`)
|
|
4534
5056
|
}
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
5057
|
+
|
|
5058
|
+
// Apply the merged renames as a single bottom-to-top position splice
|
|
5059
|
+
// over `bodySource`, using the sites collected once at detection time
|
|
5060
|
+
// (#2341 BUG-1) — never a text search, so string/template-literal
|
|
5061
|
+
// contents, property keys, `.prop` tails, and JSX intrinsic tags are
|
|
5062
|
+
// never touched, and (unlike the old sequential regex passes) an
|
|
5063
|
+
// argument expression already spliced in for an earlier site can never
|
|
5064
|
+
// be re-scanned and corrupted by a later rename.
|
|
5065
|
+
let body = factory.bodySource
|
|
5066
|
+
for (let i = factory.renameSites.length - 1; i >= 0; i--) {
|
|
5067
|
+
const site = factory.renameSites[i]
|
|
5068
|
+
const repl = renames.get(site.name)
|
|
5069
|
+
if (repl === undefined) continue
|
|
5070
|
+
const text = site.form === 'shorthand' ? `${site.name}: ${repl}` : repl
|
|
5071
|
+
body = body.slice(0, site.start) + text + body.slice(site.end)
|
|
4540
5072
|
}
|
|
4541
5073
|
|
|
4542
5074
|
edits.push({
|
|
@@ -4544,13 +5076,51 @@ function rewriteFactoryCallsInSource(
|
|
|
4544
5076
|
end: stmt.getEnd(),
|
|
4545
5077
|
replacement: body,
|
|
4546
5078
|
})
|
|
5079
|
+
inlinedFactories.add(factory)
|
|
4547
5080
|
}
|
|
4548
5081
|
|
|
4549
5082
|
visitStmt(sourceFile, false)
|
|
4550
5083
|
|
|
4551
5084
|
if (edits.length === 0) return source
|
|
4552
5085
|
|
|
4553
|
-
//
|
|
5086
|
+
// #2332 — one deduped import statement per specifier for every inlined
|
|
5087
|
+
// cross-file factory's re-provisioned imports. Injected as a zero-width
|
|
5088
|
+
// edit so the ordinary bottom-to-top splice below applies it; the result
|
|
5089
|
+
// is indistinguishable from a hand-written import for every downstream
|
|
5090
|
+
// consumer (ctx.imports → SSR templateImports AND client
|
|
5091
|
+
// collectExternalImports both parse this same rewritten string).
|
|
5092
|
+
const importsBySpecifier = new Map<string, Map<string, string>>() // specifier -> localName -> exportedName
|
|
5093
|
+
for (const f of inlinedFactories) {
|
|
5094
|
+
for (const r of f.requiredImports ?? []) {
|
|
5095
|
+
let names = importsBySpecifier.get(r.specifier)
|
|
5096
|
+
if (!names) { names = new Map(); importsBySpecifier.set(r.specifier, names) }
|
|
5097
|
+
names.set(r.localName, r.exportedName) // same-key duplicates are identical by prescan construction
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
if (importsBySpecifier.size > 0) {
|
|
5101
|
+
// Sort specifiers and, within each, named-import entries by local name —
|
|
5102
|
+
// `importsBySpecifier`/`inlinedFactories` iterate in incidental AST-
|
|
5103
|
+
// traversal/insertion order, which would otherwise make this generated
|
|
5104
|
+
// text order-unstable across unrelated refactors (Copilot review, PR
|
|
5105
|
+
// #2338).
|
|
5106
|
+
const lines = [...importsBySpecifier]
|
|
5107
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
5108
|
+
.map(([spec, names]) => {
|
|
5109
|
+
const specifiers = [...names]
|
|
5110
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
5111
|
+
.map(([local, exported]) => (exported === local ? local : `${exported} as ${local}`))
|
|
5112
|
+
return `import { ${specifiers.join(', ')} } from '${spec}'`
|
|
5113
|
+
})
|
|
5114
|
+
const at = factoryImportInsertionOffset(sourceFile)
|
|
5115
|
+
edits.push({ start: at, end: at, replacement: at === 0 ? lines.join('\n') + '\n' : '\n' + lines.join('\n') })
|
|
5116
|
+
}
|
|
5117
|
+
|
|
5118
|
+
// Apply edits from bottom to top so earlier offsets stay valid. A factory
|
|
5119
|
+
// with `requiredImports` is by definition imported, so the entry file has
|
|
5120
|
+
// at least one import statement and `at` above lands strictly inside the
|
|
5121
|
+
// import prologue — every call-site edit's start is strictly greater
|
|
5122
|
+
// (separated at minimum by the statement break after the last import), so
|
|
5123
|
+
// starts never tie and no sort tiebreak is needed.
|
|
4554
5124
|
edits.sort((a, b) => b.start - a.start)
|
|
4555
5125
|
let out = source
|
|
4556
5126
|
for (const e of edits) {
|
|
@@ -4559,6 +5129,24 @@ function rewriteFactoryCallsInSource(
|
|
|
4559
5129
|
return out
|
|
4560
5130
|
}
|
|
4561
5131
|
|
|
5132
|
+
/**
|
|
5133
|
+
* Offset just after the last top-level import (else after the 'use client'
|
|
5134
|
+
* directive, else 0) in the prescan source file — where re-provisioned
|
|
5135
|
+
* factory imports are injected (#2332).
|
|
5136
|
+
*/
|
|
5137
|
+
function factoryImportInsertionOffset(sf: ts.SourceFile): number {
|
|
5138
|
+
let lastImportEnd = -1
|
|
5139
|
+
let directiveEnd = -1
|
|
5140
|
+
for (const stmt of sf.statements) {
|
|
5141
|
+
if (ts.isImportDeclaration(stmt)) { lastImportEnd = stmt.getEnd(); continue }
|
|
5142
|
+
if (directiveEnd === -1 && ts.isExpressionStatement(stmt) &&
|
|
5143
|
+
ts.isStringLiteral(stmt.expression) && stmt.expression.text === 'use client') {
|
|
5144
|
+
directiveEnd = stmt.getEnd()
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
return lastImportEnd >= 0 ? lastImportEnd : directiveEnd >= 0 ? directiveEnd : 0
|
|
5148
|
+
}
|
|
5149
|
+
|
|
4562
5150
|
function isPascalCaseComponentFn(node: ts.Node): boolean {
|
|
4563
5151
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
4564
5152
|
return /^[A-Z]/.test(node.name.text)
|
|
@@ -4569,10 +5157,6 @@ function isPascalCaseComponentFn(node: ts.Node): boolean {
|
|
|
4569
5157
|
return false
|
|
4570
5158
|
}
|
|
4571
5159
|
|
|
4572
|
-
function escapeRegex(s: string): string {
|
|
4573
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
4574
|
-
}
|
|
4575
|
-
|
|
4576
5160
|
// =============================================================================
|
|
4577
5161
|
// BF110 diagnostic (#931)
|
|
4578
5162
|
// =============================================================================
|
|
@@ -4580,11 +5164,12 @@ function escapeRegex(s: string): string {
|
|
|
4580
5164
|
/**
|
|
4581
5165
|
* Build the diagnostic for a call site whose callee was recognised but
|
|
4582
5166
|
* declined for inlining (#2325 — cross-file factories that rename their
|
|
4583
|
-
* return properties
|
|
4584
|
-
*
|
|
4585
|
-
*
|
|
4586
|
-
*
|
|
4587
|
-
*
|
|
5167
|
+
* return properties or capture their own module scope; #2332 — a
|
|
5168
|
+
* re-provisioned helper import collides with an existing binding). BF112
|
|
5169
|
+
* and BF113's wording is specific to their respective failure; every other
|
|
5170
|
+
* declined reason (currently only BF111 — non-shorthand return properties)
|
|
5171
|
+
* shares BF111's generic "cannot be inlined: <detail>" phrasing, matching
|
|
5172
|
+
* the wording used for the tuple call-site path.
|
|
4588
5173
|
*/
|
|
4589
5174
|
function declinedFactoryMessage(callee: string, d: DeclinedReactiveFactory): string {
|
|
4590
5175
|
if (d.code === 'BF112') {
|
|
@@ -4594,9 +5179,27 @@ function declinedFactoryMessage(callee: string, d: DeclinedReactiveFactory): str
|
|
|
4594
5179
|
`pass them as factory arguments, or inline the factory here.`
|
|
4595
5180
|
)
|
|
4596
5181
|
}
|
|
5182
|
+
if (d.code === 'BF113') {
|
|
5183
|
+
return (
|
|
5184
|
+
`Reactive factory '${callee}' cannot be inlined: it needs ${d.detail} ` +
|
|
5185
|
+
`imported into this file, but that name is already bound here to something ` +
|
|
5186
|
+
`else. Rename the conflicting binding in this file, or alias the import in ` +
|
|
5187
|
+
`the factory's own file (import { x as y }).`
|
|
5188
|
+
)
|
|
5189
|
+
}
|
|
4597
5190
|
return `Reactive factory '${callee}' cannot be inlined: ${d.detail}.`
|
|
4598
5191
|
}
|
|
4599
5192
|
|
|
5193
|
+
/** Diagnostic code for a declined reactive-factory call site (#2325 / #2332). */
|
|
5194
|
+
function declinedFactoryErrorCode(code: DeclinedReactiveFactory['code']): ErrorCode {
|
|
5195
|
+
switch (code) {
|
|
5196
|
+
case 'BF112': return ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE
|
|
5197
|
+
case 'BF113': return ErrorCodes.REACTIVE_FACTORY_IMPORT_COLLISION
|
|
5198
|
+
case 'BF114': return ErrorCodes.REACTIVE_FACTORY_PARAM_SHADOWED
|
|
5199
|
+
default: return ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
|
|
4600
5203
|
/**
|
|
4601
5204
|
* Scan a compiled component context for destructures (tuple or object)
|
|
4602
5205
|
* whose callee is neither `createSignal` / `createMemo` nor an inlinable
|
|
@@ -4636,9 +5239,7 @@ export function validateReactiveFactoryCalls(ctx: AnalyzerContext): void {
|
|
|
4636
5239
|
const declinedEntry = ctx.declinedReactiveFactories.get(callee)
|
|
4637
5240
|
if (declinedEntry) {
|
|
4638
5241
|
ctx.errors.push(createError(
|
|
4639
|
-
declinedEntry.code
|
|
4640
|
-
? ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE
|
|
4641
|
-
: ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED,
|
|
5242
|
+
declinedFactoryErrorCode(declinedEntry.code),
|
|
4642
5243
|
loc,
|
|
4643
5244
|
{ severity: 'error', message: declinedFactoryMessage(callee, declinedEntry) }
|
|
4644
5245
|
))
|
|
@@ -4696,8 +5297,8 @@ export function validateReactiveFactoryCalls(ctx: AnalyzerContext): void {
|
|
|
4696
5297
|
* covers the previously-silent object-destructure failure modes: an
|
|
4697
5298
|
* unrecognised callee, a tuple factory destructured as an object, a rename/
|
|
4698
5299
|
* default/rest destructure of a shorthand-only factory, an unknown
|
|
4699
|
-
* property, a declined (BF111/BF112) factory, or an uninspectable
|
|
4700
|
-
* that looks reactive-factory-shaped by name.
|
|
5300
|
+
* property, a declined (BF111/BF112/BF113) factory, or an uninspectable
|
|
5301
|
+
* import that looks reactive-factory-shaped by name.
|
|
4701
5302
|
*/
|
|
4702
5303
|
function validateObjectFactoryDestructure(
|
|
4703
5304
|
ctx: AnalyzerContext,
|
|
@@ -4759,9 +5360,7 @@ function validateObjectFactoryDestructure(
|
|
|
4759
5360
|
const declinedEntry = ctx.declinedReactiveFactories.get(callee)
|
|
4760
5361
|
if (declinedEntry) {
|
|
4761
5362
|
ctx.errors.push(createError(
|
|
4762
|
-
declinedEntry.code
|
|
4763
|
-
? ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE
|
|
4764
|
-
: ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED,
|
|
5363
|
+
declinedFactoryErrorCode(declinedEntry.code),
|
|
4765
5364
|
loc,
|
|
4766
5365
|
{ severity: 'error', message: declinedFactoryMessage(callee, declinedEntry) }
|
|
4767
5366
|
))
|