parseman 0.46.0 → 0.47.1
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/README.md +8 -3
- package/dist/analysis/choice-cost-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +10339 -7787
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +10339 -7787
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10924 -8506
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10924 -8506
- package/dist/cli/index.js.map +3 -4
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +45 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +47 -53
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +9290 -7181
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9300 -7191
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +313 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +313 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11175 -9534
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11175 -9534
- package/dist/plugin/index.js.map +3 -4
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2934 -1611
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2934 -1611
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11463 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11440 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +453 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +496 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +26 -2
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +348 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +237 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +852 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +3006 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1777 -0
- package/src/table/emit.ts +429 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1680 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +467 -0
- package/src/table/program.ts +876 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a `node(..., build)` reducer expression to the function it names, so the
|
|
3
|
+
* capture-cost analysis reads a REAL parameter list instead of failing open.
|
|
4
|
+
*
|
|
5
|
+
* `buildSrc` is the source text of the EXPRESSION at the call site. An inline arrow is
|
|
6
|
+
* self-describing, but every other spelling — a bare identifier, a namespaced
|
|
7
|
+
* `helpers.fold`, an import from a shared module — carries no parameter list at all, and
|
|
8
|
+
* the analysis used to give up on all of them and charge the node all five capture tiers.
|
|
9
|
+
*
|
|
10
|
+
* Those spellings are not exotic; sharing reducers across a grammar and importing them
|
|
11
|
+
* from another module is ordinary grammar authoring. So this module does the work rather
|
|
12
|
+
* than reporting that it won't:
|
|
13
|
+
*
|
|
14
|
+
* - REAL SCOPE ANALYSIS. A lexical scope tree over the module, resolved by the source
|
|
15
|
+
* OFFSET of the identifier at the call site. Shadowing is decidable, so it is decided:
|
|
16
|
+
* an inner binding of the same name resolves to the inner binding, and only genuinely
|
|
17
|
+
* unreadable bindings decline. (The previous rule — "decline any name bound more than
|
|
18
|
+
* once anywhere in the module" — declined correct code to avoid thinking about this.)
|
|
19
|
+
* - `let` / `var`, admitted when the binding is never reassigned. Reassignment is
|
|
20
|
+
* detected, not assumed.
|
|
21
|
+
* - CROSS-MODULE imports: named, default, namespace-member, aliased, and re-exported.
|
|
22
|
+
* Modules are resolved and parsed once and cached by path + mtime.
|
|
23
|
+
* - ALIASES: `const fold = foldOperation` follows to the target.
|
|
24
|
+
* - AST-based parameter counting, so a DEFAULT (`(c, f = undefined, s, r) =>`, arity 4)
|
|
25
|
+
* or a destructured parameter is counted positionally instead of defeating a regex.
|
|
26
|
+
*
|
|
27
|
+
* What still declines, because it is genuinely undecidable rather than merely unread:
|
|
28
|
+
* a REST parameter (`...args` — the declared arity is unbounded), a body that references
|
|
29
|
+
* `arguments`, and a reducer that is computed, conditionally assigned, or reassigned. For
|
|
30
|
+
* those the `build-arity-unconfirmed` diagnostic fires, and `node(..., { buildArity: n })`
|
|
31
|
+
* lets the author declare the answer so the fail-open path is a true last resort.
|
|
32
|
+
*/
|
|
33
|
+
import * as fs from 'node:fs'
|
|
34
|
+
import * as path from 'node:path'
|
|
35
|
+
import { createRequire } from 'node:module'
|
|
36
|
+
import { parseSync } from 'oxc-parser'
|
|
37
|
+
|
|
38
|
+
type AnyNode = { type: string; start: number; end: number } & Record<string, unknown>
|
|
39
|
+
|
|
40
|
+
/** Why a reducer could not be reduced to a parameter list. `undefined` = it could. */
|
|
41
|
+
export type UnresolvedReason =
|
|
42
|
+
| 'rest-parameter'
|
|
43
|
+
| 'arguments'
|
|
44
|
+
| 'reassigned'
|
|
45
|
+
| 'not-a-function'
|
|
46
|
+
| 'unresolved-import'
|
|
47
|
+
| 'not-found'
|
|
48
|
+
| 'computed'
|
|
49
|
+
| 'foreign-source'
|
|
50
|
+
/** Two DISTINCT registered files hold byte-identical text, so the text no longer
|
|
51
|
+
* identifies the module an offset indexes. See `register`. */
|
|
52
|
+
| 'ambiguous-source'
|
|
53
|
+
|
|
54
|
+
export type ResolvedReducer = {
|
|
55
|
+
/** Declared positional arity, or `null` when genuinely undecidable. */
|
|
56
|
+
arity: number | null
|
|
57
|
+
/** Source text of the resolved function, for shape analysis (inline-`mk`). */
|
|
58
|
+
src: string | null
|
|
59
|
+
reason?: UnresolvedReason
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Scope tree
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
type BindingKind = 'const' | 'let' | 'var' | 'function' | 'class' | 'param' | 'import' | 'catch'
|
|
67
|
+
|
|
68
|
+
type ImportRef = { specifier: string; imported: string | '*' | 'default' }
|
|
69
|
+
|
|
70
|
+
type Binding = {
|
|
71
|
+
kind: BindingKind
|
|
72
|
+
/** Initializer / declaration node, when the binding names one directly. */
|
|
73
|
+
init?: AnyNode
|
|
74
|
+
/** Set for an `import` binding. */
|
|
75
|
+
importRef?: ImportRef
|
|
76
|
+
/** Any assignment or update to this name anywhere in its scope subtree. */
|
|
77
|
+
reassigned?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ScopeNode = {
|
|
81
|
+
start: number
|
|
82
|
+
end: number
|
|
83
|
+
bindings: Map<string, Binding>
|
|
84
|
+
children: ScopeNode[]
|
|
85
|
+
parent: ScopeNode | null
|
|
86
|
+
/** `var` and function declarations hoist to the nearest scope with this set. */
|
|
87
|
+
isFunction: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const FUNCTION_TYPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])
|
|
91
|
+
const SCOPE_TYPES = new Set([
|
|
92
|
+
...FUNCTION_TYPES, 'BlockStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement',
|
|
93
|
+
'CatchClause', 'ClassDeclaration', 'ClassExpression', 'StaticBlock',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
function newScope(start: number, end: number, parent: ScopeNode | null, isFunction: boolean): ScopeNode {
|
|
97
|
+
const s: ScopeNode = { start, end, bindings: new Map(), children: [], parent, isFunction }
|
|
98
|
+
parent?.children.push(s)
|
|
99
|
+
return s
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Every identifier a binding pattern introduces. */
|
|
103
|
+
function patternNames(pat: unknown, out: string[]): void {
|
|
104
|
+
if (!pat || typeof pat !== 'object') return
|
|
105
|
+
const n = pat as AnyNode
|
|
106
|
+
switch (n.type) {
|
|
107
|
+
case 'Identifier': out.push(n.name as string); return
|
|
108
|
+
case 'AssignmentPattern': return patternNames(n.left, out)
|
|
109
|
+
case 'RestElement': return patternNames(n.argument, out)
|
|
110
|
+
case 'ArrayPattern': for (const el of (n.elements as unknown[]) ?? []) patternNames(el, out); return
|
|
111
|
+
case 'ObjectPattern':
|
|
112
|
+
for (const p of (n.properties as AnyNode[]) ?? []) patternNames(p.type === 'RestElement' ? p.argument : p.value, out)
|
|
113
|
+
return
|
|
114
|
+
// A TS parameter property / assignment target wrapper.
|
|
115
|
+
case 'TSParameterProperty': return patternNames(n.parameter, out)
|
|
116
|
+
default: return
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function declare(scope: ScopeNode, name: string, b: Binding): void {
|
|
121
|
+
// First declaration wins; a duplicate `var` re-declaration does not change the shape.
|
|
122
|
+
if (!scope.bindings.has(name)) scope.bindings.set(name, b)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `var` and function declarations hoist to the nearest FUNCTION scope, not the block. */
|
|
126
|
+
function functionScope(scope: ScopeNode): ScopeNode {
|
|
127
|
+
let s: ScopeNode = scope
|
|
128
|
+
while (!s.isFunction && s.parent) s = s.parent
|
|
129
|
+
return s
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildScopeTree(body: unknown[], moduleEnd: number): ScopeNode {
|
|
133
|
+
const root = newScope(0, moduleEnd, null, true)
|
|
134
|
+
|
|
135
|
+
const declarePattern = (scope: ScopeNode, pat: unknown, kind: BindingKind, init?: AnyNode): void => {
|
|
136
|
+
const names: string[] = []
|
|
137
|
+
patternNames(pat, names)
|
|
138
|
+
const target = kind === 'var' ? functionScope(scope) : scope
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
declare(target, name, init !== undefined && names.length === 1 ? { kind, init } : { kind })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const walk = (n: unknown, scope: ScopeNode): void => {
|
|
145
|
+
if (!n || typeof n !== 'object') return
|
|
146
|
+
if (Array.isArray(n)) { for (const item of n) walk(item, scope); return }
|
|
147
|
+
const node = n as AnyNode
|
|
148
|
+
if (typeof node.type !== 'string') return
|
|
149
|
+
|
|
150
|
+
// --- declarations, in the CURRENT scope ---
|
|
151
|
+
switch (node.type) {
|
|
152
|
+
case 'VariableDeclaration': {
|
|
153
|
+
const kind = (node.kind as BindingKind) ?? 'var'
|
|
154
|
+
for (const d of (node.declarations as AnyNode[]) ?? []) {
|
|
155
|
+
declarePattern(scope, d.id, kind, (d.init as AnyNode | null) ?? undefined)
|
|
156
|
+
}
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
case 'FunctionDeclaration':
|
|
160
|
+
if (node.id) declare(functionScope(scope), (node.id as AnyNode).name as string, { kind: 'function', init: node })
|
|
161
|
+
break
|
|
162
|
+
case 'ClassDeclaration':
|
|
163
|
+
if (node.id) declare(scope, (node.id as AnyNode).name as string, { kind: 'class' })
|
|
164
|
+
break
|
|
165
|
+
case 'ImportDeclaration': {
|
|
166
|
+
const specifier = ((node.source as AnyNode | undefined)?.value as string) ?? ''
|
|
167
|
+
for (const sp of (node.specifiers as AnyNode[]) ?? []) {
|
|
168
|
+
const local = (sp.local as AnyNode | undefined)?.name as string | undefined
|
|
169
|
+
if (!local) continue
|
|
170
|
+
const imported = sp.type === 'ImportDefaultSpecifier'
|
|
171
|
+
? 'default'
|
|
172
|
+
: sp.type === 'ImportNamespaceSpecifier'
|
|
173
|
+
? '*'
|
|
174
|
+
: ((sp.imported as AnyNode | undefined)?.name as string) ?? local
|
|
175
|
+
declare(scope, local, { kind: 'import', importRef: { specifier, imported } })
|
|
176
|
+
}
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
default: break
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- new scopes ---
|
|
183
|
+
if (SCOPE_TYPES.has(node.type)) {
|
|
184
|
+
const isFn = FUNCTION_TYPES.has(node.type)
|
|
185
|
+
const inner = newScope(node.start, node.end, scope, isFn)
|
|
186
|
+
if (isFn) {
|
|
187
|
+
for (const p of (node.params as unknown[]) ?? []) declarePattern(inner, p, 'param')
|
|
188
|
+
// A named function EXPRESSION binds its own name inside its body.
|
|
189
|
+
if (node.type === 'FunctionExpression' && node.id) {
|
|
190
|
+
declare(inner, (node.id as AnyNode).name as string, { kind: 'function', init: node })
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (node.type === 'CatchClause' && node.param) declarePattern(inner, node.param, 'catch')
|
|
194
|
+
for (const key of Object.keys(node)) {
|
|
195
|
+
if (key === 'type' || key === 'start' || key === 'end') continue
|
|
196
|
+
walk(node[key], inner)
|
|
197
|
+
}
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const key of Object.keys(node)) {
|
|
202
|
+
if (key === 'type' || key === 'start' || key === 'end') continue
|
|
203
|
+
walk(node[key], scope)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const stmt of body) walk(stmt, root)
|
|
208
|
+
return root
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Innermost scope containing `offset`. */
|
|
212
|
+
function scopeAt(root: ScopeNode, offset: number): ScopeNode {
|
|
213
|
+
let cur = root
|
|
214
|
+
for (;;) {
|
|
215
|
+
const next = cur.children.find(c => offset >= c.start && offset <= c.end)
|
|
216
|
+
if (!next) return cur
|
|
217
|
+
cur = next
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function lookup(scope: ScopeNode, name: string): { binding: Binding; scope: ScopeNode } | null {
|
|
222
|
+
for (let s: ScopeNode | null = scope; s; s = s.parent) {
|
|
223
|
+
const b = s.bindings.get(name)
|
|
224
|
+
if (b) return { binding: b, scope: s }
|
|
225
|
+
}
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Mark every binding that is assigned or updated after declaration. This is what makes a
|
|
231
|
+
* `let` admissible: "never reassigned" is a decidable property, and declining every
|
|
232
|
+
* `let` because one of them *might* be reassigned is the guesswork this module exists to
|
|
233
|
+
* remove.
|
|
234
|
+
*/
|
|
235
|
+
function markReassignments(body: unknown[], root: ScopeNode): void {
|
|
236
|
+
const walk = (n: unknown): void => {
|
|
237
|
+
if (!n || typeof n !== 'object') return
|
|
238
|
+
if (Array.isArray(n)) { for (const item of n) walk(item); return }
|
|
239
|
+
const node = n as AnyNode
|
|
240
|
+
if (typeof node.type !== 'string') return
|
|
241
|
+
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
|
|
242
|
+
const target = (node.type === 'UpdateExpression' ? node.argument : node.left) as AnyNode | undefined
|
|
243
|
+
const names: string[] = []
|
|
244
|
+
if (target) patternNames(target, names)
|
|
245
|
+
for (const name of names) {
|
|
246
|
+
const hit = lookup(scopeAt(root, node.start), name)
|
|
247
|
+
if (hit) hit.binding.reassigned = true
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const key of Object.keys(node)) {
|
|
251
|
+
if (key === 'type' || key === 'start' || key === 'end') continue
|
|
252
|
+
walk(node[key])
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const stmt of body) walk(stmt)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Parameter list → arity, from the AST
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Declared positional arity of a function node, or `null` when undecidable.
|
|
264
|
+
*
|
|
265
|
+
* A DEFAULT does not change positional arity (`(c, f = undefined, s, r)` is 4) and a
|
|
266
|
+
* DESTRUCTURED parameter still occupies one position — both were previously rejected by
|
|
267
|
+
* a regex that only accepted plain identifiers. A REST parameter genuinely is undecidable
|
|
268
|
+
* (the declared arity is unbounded), as is a body that reads `arguments`.
|
|
269
|
+
*/
|
|
270
|
+
export function functionArity(fn: AnyNode): { arity: number | null; reason?: UnresolvedReason } {
|
|
271
|
+
const params = (fn.params as AnyNode[]) ?? []
|
|
272
|
+
let count = 0
|
|
273
|
+
for (const p of params) {
|
|
274
|
+
if (p.type === 'RestElement') return { arity: null, reason: 'rest-parameter' }
|
|
275
|
+
// A TypeScript `this` parameter is not a positional argument.
|
|
276
|
+
if (p.type === 'Identifier' && p.name === 'this') continue
|
|
277
|
+
count++
|
|
278
|
+
}
|
|
279
|
+
if (fn.type !== 'ArrowFunctionExpression' && referencesArguments(fn.body)) {
|
|
280
|
+
return { arity: null, reason: 'arguments' }
|
|
281
|
+
}
|
|
282
|
+
return { arity: count }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** `arguments` in this function's own body — not a nested non-arrow function's. */
|
|
286
|
+
function referencesArguments(n: unknown): boolean {
|
|
287
|
+
if (!n || typeof n !== 'object') return false
|
|
288
|
+
if (Array.isArray(n)) return n.some(referencesArguments)
|
|
289
|
+
const node = n as AnyNode
|
|
290
|
+
if (typeof node.type !== 'string') return false
|
|
291
|
+
if (node.type === 'Identifier' && node.name === 'arguments') return true
|
|
292
|
+
// A nested non-arrow function has its own `arguments`; an arrow inherits ours.
|
|
293
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') return false
|
|
294
|
+
for (const key of Object.keys(node)) {
|
|
295
|
+
if (key === 'type' || key === 'start' || key === 'end') continue
|
|
296
|
+
if (referencesArguments(node[key])) return true
|
|
297
|
+
}
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Module graph
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
type AnalysedModule = {
|
|
306
|
+
file: string
|
|
307
|
+
src: string
|
|
308
|
+
body: unknown[]
|
|
309
|
+
scopes: ScopeNode
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const _moduleCache = new Map<string, { mtimeMs: number; mod: AnalysedModule | null }>()
|
|
313
|
+
|
|
314
|
+
function analyseModuleFile(file: string): AnalysedModule | null {
|
|
315
|
+
let mtimeMs: number
|
|
316
|
+
try { mtimeMs = fs.statSync(file).mtimeMs } catch { return null }
|
|
317
|
+
const hit = _moduleCache.get(file)
|
|
318
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.mod
|
|
319
|
+
let mod: AnalysedModule | null = null
|
|
320
|
+
try {
|
|
321
|
+
const src = fs.readFileSync(file, 'utf8')
|
|
322
|
+
const r = parseSync(file, src)
|
|
323
|
+
if (r.errors.length === 0) {
|
|
324
|
+
const body = r.program.body as unknown[]
|
|
325
|
+
const scopes = buildScopeTree(body, src.length)
|
|
326
|
+
markReassignments(body, scopes)
|
|
327
|
+
mod = { file, src, body, scopes }
|
|
328
|
+
}
|
|
329
|
+
} catch { /* leave null */ }
|
|
330
|
+
_moduleCache.set(file, { mtimeMs, mod })
|
|
331
|
+
return mod
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const SOURCE_EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx']
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Resolve an import specifier to a readable file.
|
|
338
|
+
*
|
|
339
|
+
* Deliberately broader than `resolvePrivateSourceModule` in `index.ts`: that one
|
|
340
|
+
* restricts to the importing package because it LOWERS the target into the output. Here
|
|
341
|
+
* we only READ a parameter list, so a workspace sibling or an installed package is
|
|
342
|
+
* perfectly safe to inspect — and refusing to look is exactly how "imported reducer"
|
|
343
|
+
* became a permanent fail-open.
|
|
344
|
+
*/
|
|
345
|
+
function resolveImport(fromFile: string, specifier: string): string | null {
|
|
346
|
+
if (specifier.startsWith('.') || path.isAbsolute(specifier)) {
|
|
347
|
+
const base = path.resolve(path.dirname(fromFile), specifier)
|
|
348
|
+
const ext = path.extname(base)
|
|
349
|
+
const stem = ext ? base.slice(0, -ext.length) : base
|
|
350
|
+
const candidates = [
|
|
351
|
+
...(ext ? [base] : []),
|
|
352
|
+
// `./x.js` in TS source usually means `./x.ts`.
|
|
353
|
+
...SOURCE_EXTS.map(e => `${stem}${e}`),
|
|
354
|
+
...SOURCE_EXTS.map(e => path.join(base, `index${e}`)),
|
|
355
|
+
]
|
|
356
|
+
for (const c of candidates) {
|
|
357
|
+
try { if (fs.statSync(c).isFile()) return fs.realpathSync(c) } catch { /* next */ }
|
|
358
|
+
}
|
|
359
|
+
return null
|
|
360
|
+
}
|
|
361
|
+
// A bare package specifier — a workspace sibling or an installed package. Node's own
|
|
362
|
+
// resolution from the importing file, which is what the built program will use.
|
|
363
|
+
try {
|
|
364
|
+
return fs.realpathSync(createRequire(fromFile).resolve(specifier))
|
|
365
|
+
} catch { return null }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Does this `export <decl>` actually introduce `name`? */
|
|
369
|
+
function declarationBinds(decl: AnyNode, name: string): boolean {
|
|
370
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
371
|
+
return ((decl.id as AnyNode | undefined)?.name as string | undefined) === name
|
|
372
|
+
}
|
|
373
|
+
if (decl.type === 'VariableDeclaration') {
|
|
374
|
+
const names: string[] = []
|
|
375
|
+
for (const d of (decl.declarations as AnyNode[]) ?? []) patternNames(d.id, names)
|
|
376
|
+
return names.includes(name)
|
|
377
|
+
}
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Find what a module EXPORTS under `name` ('default' for a default export). */
|
|
382
|
+
function exportedBinding(
|
|
383
|
+
mod: AnalysedModule,
|
|
384
|
+
name: string,
|
|
385
|
+
seen: Set<string>,
|
|
386
|
+
): { mod: AnalysedModule; binding: Binding } | null {
|
|
387
|
+
for (const stmt of mod.body as AnyNode[]) {
|
|
388
|
+
if (stmt.type === 'ExportDefaultDeclaration' && name === 'default') {
|
|
389
|
+
const d = stmt.declaration as AnyNode
|
|
390
|
+
if (FUNCTION_TYPES.has(d.type)) return { mod, binding: { kind: 'const', init: d } }
|
|
391
|
+
if (d.type === 'Identifier') {
|
|
392
|
+
const hit = lookup(mod.scopes, d.name as string)
|
|
393
|
+
return hit ? { mod, binding: hit.binding } : null
|
|
394
|
+
}
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
if (stmt.type === 'ExportNamedDeclaration') {
|
|
398
|
+
const decl = stmt.declaration as AnyNode | undefined
|
|
399
|
+
if (decl && declarationBinds(decl, name)) {
|
|
400
|
+
// `export const f = …` / `export function f() {}` — already in module scope.
|
|
401
|
+
const hit = lookup(mod.scopes, name)
|
|
402
|
+
if (hit && hit.scope === mod.scopes) return { mod, binding: hit.binding }
|
|
403
|
+
}
|
|
404
|
+
for (const sp of (stmt.specifiers as AnyNode[]) ?? []) {
|
|
405
|
+
const exported = ((sp.exported as AnyNode | undefined)?.name as string) ?? ''
|
|
406
|
+
if (exported !== name) continue
|
|
407
|
+
const local = ((sp.local as AnyNode | undefined)?.name as string) ?? exported
|
|
408
|
+
const from = (stmt.source as AnyNode | undefined)?.value as string | undefined
|
|
409
|
+
if (from !== undefined) return followReExport(mod, from, local, seen)
|
|
410
|
+
const hit = lookup(mod.scopes, local)
|
|
411
|
+
return hit ? { mod, binding: hit.binding } : null
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// `export * from './y'` — search the target.
|
|
415
|
+
if (stmt.type === 'ExportAllDeclaration' && !stmt.exported) {
|
|
416
|
+
const from = (stmt.source as AnyNode | undefined)?.value as string | undefined
|
|
417
|
+
if (from === undefined) continue
|
|
418
|
+
const found = followReExport(mod, from, name, seen)
|
|
419
|
+
if (found) return found
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// A plain module-scope declaration that the file exports elsewhere, or a module we
|
|
423
|
+
// parsed from compiled output where the export list is a bundler rename.
|
|
424
|
+
const hit = lookup(mod.scopes, name)
|
|
425
|
+
return hit && hit.scope === mod.scopes ? { mod, binding: hit.binding } : null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function followReExport(
|
|
429
|
+
mod: AnalysedModule,
|
|
430
|
+
specifier: string,
|
|
431
|
+
name: string,
|
|
432
|
+
seen: Set<string>,
|
|
433
|
+
): { mod: AnalysedModule; binding: Binding } | null {
|
|
434
|
+
const file = resolveImport(mod.file, specifier)
|
|
435
|
+
if (!file || seen.has(`${file}\0${name}`)) return null
|
|
436
|
+
seen.add(`${file}\0${name}`)
|
|
437
|
+
const next = analyseModuleFile(file)
|
|
438
|
+
return next ? exportedBinding(next, name, seen) : null
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// The resolver
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
const MAX_HOPS = 12
|
|
446
|
+
|
|
447
|
+
export type ReducerResolver = {
|
|
448
|
+
/**
|
|
449
|
+
* Resolve the reducer expression at `offset` (its source text is `exprSrc`).
|
|
450
|
+
* Returns `null` when the expression is not a name at all (an inline function —
|
|
451
|
+
* the caller already has its source and needs nothing from us).
|
|
452
|
+
*
|
|
453
|
+
* `src` is the module text `offset` indexes. An offset is only meaningful against the
|
|
454
|
+
* source it came from, so the caller must say which one it has; see `resolve`'s
|
|
455
|
+
* implementation for what happens when it is not this resolver's.
|
|
456
|
+
*/
|
|
457
|
+
resolve(exprSrc: string, offset: number, src: string): ResolvedReducer | null
|
|
458
|
+
/**
|
|
459
|
+
* Announce a module whose text the evaluator may later hand to `resolve` as `src` —
|
|
460
|
+
* an imported `rules()` factory is evaluated with `code: mod.src`, so its `node(…,
|
|
461
|
+
* build)` offsets index THAT file, not the entry file.
|
|
462
|
+
*
|
|
463
|
+
* Registering it makes those offsets answerable against the module's own scope tree.
|
|
464
|
+
* Without it `resolve` can only decline (`foreign-source`), which fails open to full
|
|
465
|
+
* capture — correct, but it charges every such node all five facilities.
|
|
466
|
+
*
|
|
467
|
+
* Returns whether the file could be analysed; an unreadable or unparseable module
|
|
468
|
+
* simply stays unregistered and keeps declining.
|
|
469
|
+
*/
|
|
470
|
+
register(file: string): boolean
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const IDENT_RE = /^[A-Za-z_$][\w$]*$/
|
|
474
|
+
const MEMBER_RE = /^([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)$/
|
|
475
|
+
|
|
476
|
+
export function createReducerResolver(entryFile: string, body: unknown[], src: string): ReducerResolver {
|
|
477
|
+
const scopes = buildScopeTree(body, src.length)
|
|
478
|
+
markReassignments(body, scopes)
|
|
479
|
+
const entry: AnalysedModule = { file: entryFile, src, body, scopes }
|
|
480
|
+
|
|
481
|
+
// Modules OTHER than the entry file whose offsets `resolve` may be handed, keyed by
|
|
482
|
+
// the module text itself — that is the only identity the evaluator carries, since it
|
|
483
|
+
// threads `code` (a string) and not a path.
|
|
484
|
+
const registered = new Map<string, AnalysedModule>()
|
|
485
|
+
// Texts claimed by more than one DISTINCT file. Identical text is identical for
|
|
486
|
+
// offsets, but NOT for import resolution: `fromBinding` follows a hop with
|
|
487
|
+
// `resolveImport(mod.file, specifier)`, and identical text means identical RELATIVE
|
|
488
|
+
// specifiers, which resolve to different absolute files from different directories.
|
|
489
|
+
// Letting the second registration overwrite the first would answer such an offset
|
|
490
|
+
// against the wrong module and yield a wrong arity — and a wrong arity UNDER-captures,
|
|
491
|
+
// calling the reducer with arguments the compiler elided. That is the exact failure
|
|
492
|
+
// this registry exists to prevent, so an ambiguous key declines instead.
|
|
493
|
+
const ambiguous = new Set<string>()
|
|
494
|
+
|
|
495
|
+
const fromBinding = (
|
|
496
|
+
mod: AnalysedModule,
|
|
497
|
+
binding: Binding,
|
|
498
|
+
memberName: string | undefined,
|
|
499
|
+
hops: number,
|
|
500
|
+
seen: Set<string>,
|
|
501
|
+
): ResolvedReducer => {
|
|
502
|
+
if (hops > MAX_HOPS) return { arity: null, src: null, reason: 'not-found' }
|
|
503
|
+
|
|
504
|
+
// A reassigned binding may hold a different function by the time it is read.
|
|
505
|
+
if (binding.reassigned) return { arity: null, src: null, reason: 'reassigned' }
|
|
506
|
+
|
|
507
|
+
if (binding.kind === 'param' || binding.kind === 'catch' || binding.kind === 'class') {
|
|
508
|
+
return { arity: null, src: null, reason: 'not-a-function' }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (binding.kind === 'import' && binding.importRef) {
|
|
512
|
+
const { specifier, imported } = binding.importRef
|
|
513
|
+
// `import * as ns` used without a member names a MODULE, not a function. Decide
|
|
514
|
+
// that before touching the filesystem, so the reason is the real one rather than
|
|
515
|
+
// whatever the resolver happened to say about the path.
|
|
516
|
+
if (imported === '*' && memberName === undefined) return { arity: null, src: null, reason: 'not-a-function' }
|
|
517
|
+
const file = resolveImport(mod.file, specifier)
|
|
518
|
+
if (!file) return { arity: null, src: null, reason: 'unresolved-import' }
|
|
519
|
+
const next = analyseModuleFile(file)
|
|
520
|
+
if (!next) return { arity: null, src: null, reason: 'unresolved-import' }
|
|
521
|
+
// `import * as helpers` + `helpers.fold` — the member names the export.
|
|
522
|
+
const wanted = imported === '*' ? memberName : imported
|
|
523
|
+
if (wanted === undefined) return { arity: null, src: null, reason: 'not-a-function' }
|
|
524
|
+
const found = exportedBinding(next, wanted, seen)
|
|
525
|
+
if (!found) return { arity: null, src: null, reason: 'not-found' }
|
|
526
|
+
return fromBinding(found.mod, found.binding, undefined, hops + 1, seen)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const init = binding.init
|
|
530
|
+
if (!init) return { arity: null, src: null, reason: 'not-a-function' }
|
|
531
|
+
|
|
532
|
+
// `const fold = foldOperation` — follow the alias.
|
|
533
|
+
if (init.type === 'Identifier') {
|
|
534
|
+
const hit = lookup(scopeAt(mod.scopes, init.start), init.name as string)
|
|
535
|
+
if (!hit) return { arity: null, src: null, reason: 'not-found' }
|
|
536
|
+
return fromBinding(mod, hit.binding, undefined, hops + 1, seen)
|
|
537
|
+
}
|
|
538
|
+
// `const fold = helpers.foldOperation`. A COMPUTED access (`table[key]`) names
|
|
539
|
+
// something only the running program knows — that is genuine undecidability, and it
|
|
540
|
+
// reports as such rather than as a missing binding.
|
|
541
|
+
if (init.type === 'ComputedMemberExpression' || (init.computed === true && init.type.endsWith('MemberExpression'))) {
|
|
542
|
+
return { arity: null, src: null, reason: 'computed' }
|
|
543
|
+
}
|
|
544
|
+
if (init.type === 'StaticMemberExpression' || init.type === 'MemberExpression') {
|
|
545
|
+
const obj = init.object as AnyNode | undefined
|
|
546
|
+
const prop = init.property as AnyNode | undefined
|
|
547
|
+
if (obj?.type === 'Identifier' && prop?.type === 'Identifier') {
|
|
548
|
+
const hit = lookup(scopeAt(mod.scopes, init.start), obj.name as string)
|
|
549
|
+
if (!hit) return { arity: null, src: null, reason: 'not-found' }
|
|
550
|
+
return fromBinding(mod, hit.binding, prop.name as string, hops + 1, seen)
|
|
551
|
+
}
|
|
552
|
+
return { arity: null, src: null, reason: 'computed' }
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!FUNCTION_TYPES.has(init.type)) return { arity: null, src: null, reason: 'not-a-function' }
|
|
556
|
+
|
|
557
|
+
const { arity, reason } = functionArity(init)
|
|
558
|
+
return { arity, src: mod.src.slice(init.start, init.end), ...(reason ? { reason } : {}) }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
register(file) {
|
|
563
|
+
const mod = analyseModuleFile(file)
|
|
564
|
+
if (!mod) return false
|
|
565
|
+
// The entry file needs no entry — it is matched first, and its `body`/`src` came
|
|
566
|
+
// from the caller rather than a re-read, so they are the authoritative pair.
|
|
567
|
+
if (mod.src === src) return true
|
|
568
|
+
const prior = registered.get(mod.src)
|
|
569
|
+
if (prior !== undefined && prior.file !== mod.file) {
|
|
570
|
+
ambiguous.add(mod.src)
|
|
571
|
+
registered.delete(mod.src)
|
|
572
|
+
return false
|
|
573
|
+
}
|
|
574
|
+
if (!ambiguous.has(mod.src)) registered.set(mod.src, mod)
|
|
575
|
+
return !ambiguous.has(mod.src)
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
resolve(exprSrc, offset, offsetSrc) {
|
|
579
|
+
const text = exprSrc.trim()
|
|
580
|
+
const ident = IDENT_RE.test(text) ? text : null
|
|
581
|
+
const member = ident ? null : MEMBER_RE.exec(text)
|
|
582
|
+
if (!ident && !member) return null // an inline function, or something computed
|
|
583
|
+
|
|
584
|
+
// An IMPORTED `rules()` factory is evaluated with `code: mod.src` (plugin/index.ts),
|
|
585
|
+
// so a `node(..., build)` inside it carries an offset into THAT file. Indexing it
|
|
586
|
+
// into the entry file's scope tree is not a near miss: it silently names whatever
|
|
587
|
+
// binding happens to live at the same absolute offset, and a wrong arity UNDER-
|
|
588
|
+
// captures — the reducer is then called with arguments the compiler elided.
|
|
589
|
+
//
|
|
590
|
+
// So the offset picks its OWN module: the entry file, or one the plugin registered
|
|
591
|
+
// when it decided to evaluate a factory out of it. An offset from anywhere else is
|
|
592
|
+
// still refused, which fails open to full capture and says so on the degradation
|
|
593
|
+
// channel — never answered against the wrong scope tree.
|
|
594
|
+
const mod = offsetSrc === src ? entry : registered.get(offsetSrc)
|
|
595
|
+
if (!mod) {
|
|
596
|
+
return { arity: null, src: null, reason: ambiguous.has(offsetSrc) ? 'ambiguous-source' : 'foreign-source' }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const name = ident ?? member![1]!
|
|
600
|
+
const memberName = ident ? undefined : member![2]!
|
|
601
|
+
const hit = lookup(scopeAt(mod.scopes, offset), name)
|
|
602
|
+
if (!hit) return { arity: null, src: null, reason: 'not-found' }
|
|
603
|
+
return fromBinding(mod, hit.binding, memberName, 0, new Set())
|
|
604
|
+
},
|
|
605
|
+
}
|
|
606
|
+
}
|