@intentius/chant 0.28.0 → 0.29.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/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +5 -3
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/config.d.ts +46 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +153 -17
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-wire.d.ts +3 -2
- package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
- package/dist/env.d.ts +5 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +12 -0
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +29 -4
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +27 -0
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon.d.ts +31 -6
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +26 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/live-diff.d.ts +25 -1
- package/dist/lifecycle/live-diff.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +4 -2
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +26 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +8 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +92 -0
- package/dist/live-endpoint.d.ts.map +1 -0
- package/dist/observation.d.ts +123 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.test.ts +63 -4
- package/src/cli/handlers/components.ts +78 -35
- package/src/cli/handlers/graph.test.ts +69 -6
- package/src/cli/handlers/graph.ts +61 -27
- package/src/cli/handlers/lifecycle.test.ts +285 -6
- package/src/cli/handlers/lifecycle.ts +297 -185
- package/src/config.test.ts +75 -0
- package/src/config.ts +61 -3
- package/src/discovery/fold-composite.test.ts +594 -0
- package/src/discovery/fold-import.ts +987 -43
- package/src/discovery/sandbox/config-wire.ts +3 -2
- package/src/env.test.ts +12 -0
- package/src/env.ts +12 -4
- package/src/fold/fold.ts +12 -2
- package/src/graph-ir-live.test.ts +28 -1
- package/src/graph-ir.ts +68 -12
- package/src/index.ts +1 -0
- package/src/kubectl-context.ts +81 -0
- package/src/lexicon.ts +41 -6
- package/src/lifecycle/change-set.test.ts +93 -1
- package/src/lifecycle/change-set.ts +65 -13
- package/src/lifecycle/live-diff.test.ts +39 -0
- package/src/lifecycle/live-diff.ts +51 -5
- package/src/lifecycle/observe.test.ts +74 -3
- package/src/lifecycle/observe.ts +82 -22
- package/src/lifecycle/snapshot.test.ts +39 -1
- package/src/lifecycle/snapshot.ts +34 -9
- package/src/lifecycle/status.test.ts +89 -8
- package/src/lifecycle/status.ts +53 -3
- package/src/lifecycle/types.ts +8 -0
- package/src/live-endpoint.test.ts +115 -0
- package/src/live-endpoint.ts +148 -0
- package/src/observation.test.ts +96 -0
- package/src/observation.ts +213 -0
- package/src/stack-output.test.ts +55 -0
- package/src/stack-output.ts +41 -20
|
@@ -5,13 +5,14 @@ import { dirname, basename, join, sep, isAbsolute, resolve as resolvePath } from
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
8
|
-
import { isCompositeInstance, type CompositeInstance } from "../composite";
|
|
8
|
+
import { Composite, isCompositeInstance, type CompositeInstance, type CompositeMembers } from "../composite";
|
|
9
9
|
import { isAttrRefLike } from "../utils";
|
|
10
10
|
import { isIntrinsic } from "../intrinsic";
|
|
11
11
|
import {
|
|
12
12
|
collectConsts,
|
|
13
13
|
foldResource,
|
|
14
14
|
fold,
|
|
15
|
+
propName,
|
|
15
16
|
FoldError,
|
|
16
17
|
locate,
|
|
17
18
|
type FoldedResource,
|
|
@@ -21,7 +22,18 @@ import {
|
|
|
21
22
|
type SymbolicValue,
|
|
22
23
|
} from "../fold/fold";
|
|
23
24
|
import { isChantOwnedSpecifier, isFoldableHelperName } from "../fold/foldable-helpers";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
briefNodeText,
|
|
27
|
+
callExpressionMessage,
|
|
28
|
+
computedPropertyNameMessage,
|
|
29
|
+
isLiteralElementKey,
|
|
30
|
+
unsupportedBinaryMessage,
|
|
31
|
+
unsupportedExpressionMessage,
|
|
32
|
+
UNSUPPORTED_OBJECT_MEMBER_MESSAGE,
|
|
33
|
+
UNSUPPORTED_UNARY_MESSAGE,
|
|
34
|
+
SUPPORTED_BINARY_OPERATORS,
|
|
35
|
+
SUPPORTED_UNARY_OPERATORS,
|
|
36
|
+
} from "../fold/subset";
|
|
25
37
|
import { importModule } from "./import";
|
|
26
38
|
import type { IntrinsicDef } from "../lexicon";
|
|
27
39
|
import type { BuildParamValue } from "../build-params";
|
|
@@ -44,28 +56,37 @@ import type { BuildParamValue } from "../build-params";
|
|
|
44
56
|
* class/function; the only thing skipped here is executing the file's *own*
|
|
45
57
|
* statements.
|
|
46
58
|
*
|
|
47
|
-
* chant #
|
|
48
|
-
*
|
|
59
|
+
* chant #1022 extends this from leaf resources (`new Type(...)`) to composite
|
|
60
|
+
* factory calls — `SomeComposite({...})`,
|
|
49
61
|
* `propagate(SomeComposite({...}), {...})`, member access on the result
|
|
50
62
|
* (`web.deployment`), and destructuring (`const { a, b } = SomeComposite(...)`
|
|
51
63
|
* or `export const { a, b } = SomeComposite(...)`). A composite factory is a
|
|
52
64
|
* pure function of its props (EVL009/EVL010 guarantee its body only
|
|
53
65
|
* references props, sibling members, and imports), so — exactly like a
|
|
54
|
-
* resource constructor — it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* resource constructor — it is resolved through the file's imports and
|
|
67
|
+
* INVOKED with statically-folded props, and the RESULT is what matters: if it
|
|
68
|
+
* satisfies {@link isCompositeInstance} (or, for a plain resource-returning
|
|
69
|
+
* helper, {@link isDeclarable}), it's used. Nested composites and
|
|
70
|
+
* `propagate()`'d shared props need no special-casing there: a nested
|
|
71
|
+
* composite is just another member the real factory call already produced,
|
|
72
|
+
* and `propagate` is just another resolvable imported function that receives a
|
|
73
|
+
* live `CompositeInstance` plus folded shared props and returns it —
|
|
74
|
+
* `expandComposite()` (invoked downstream by `collectEntities`, unchanged)
|
|
75
|
+
* does the recursive expansion and the shared-prop merge exactly as it does
|
|
76
|
+
* for the run path.
|
|
77
|
+
*
|
|
78
|
+
* chant #1023 (epic #1019 Phase 5) removes that invocation where it can. When
|
|
79
|
+
* the callee is a composite defined in a PROJECT file as
|
|
80
|
+
* `Composite(<fn>, "<name>")` and `<fn>`'s body stays inside a closed,
|
|
81
|
+
* documented subset, its body is INTERPRETED instead — the defining module is
|
|
82
|
+
* never imported, and the members are built here, by the lexicon's own
|
|
83
|
+
* constructors, from the same folded props. That closes the last place a file
|
|
84
|
+
* reported as `[fold:fold]` could still execute project-authored code in the
|
|
85
|
+
* CLI's process (#1093), and lets such a file fold under `--sandbox` rather
|
|
86
|
+
* than being demoted to the child (#1111). A factory outside the subset keeps
|
|
87
|
+
* invoking, unchanged. See the contract block above
|
|
88
|
+
* {@link resolveInterpretableFactory} for the five admissibility rules and
|
|
89
|
+
* what interpretation preserves.
|
|
69
90
|
*/
|
|
70
91
|
|
|
71
92
|
/** One exported `const` name folded to a real, constructed `Declarable` or `CompositeInstance`. */
|
|
@@ -217,6 +238,25 @@ export interface FoldSession {
|
|
|
217
238
|
* only the fold half would buy nothing there.
|
|
218
239
|
*/
|
|
219
240
|
readonly sandbox: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* chant #1023 — per-build memo for {@link readFactoryModule}, keyed by the
|
|
243
|
+
* resolved absolute path of a module that DEFINES a composite. A composite
|
|
244
|
+
* defined once and called from a dozen sibling files is parsed, and its
|
|
245
|
+
* imports resolved, exactly once per build — the same reason
|
|
246
|
+
* {@link FoldSession.cache} exists for the files discovery folds directly.
|
|
247
|
+
*
|
|
248
|
+
* Separate from `cache` because the two ask different questions of the same
|
|
249
|
+
* file: `cache` asks "what are this module's exported VALUES" (and fails
|
|
250
|
+
* outright for a module that exports a function declaration, which a
|
|
251
|
+
* composite-defining module very often does); this asks "what is this
|
|
252
|
+
* module's static SCOPE" — its consts, its imports, and those imports'
|
|
253
|
+
* already-resolved values — which is well-defined even when the module as a
|
|
254
|
+
* whole doesn't fold. `lexicons/aws/examples/lambda-api/src/lambda-api.ts`
|
|
255
|
+
* is exactly that case: it exports two plain functions alongside its
|
|
256
|
+
* `Composite`, so it never folds, and its `LambdaApi` is interpretable
|
|
257
|
+
* regardless.
|
|
258
|
+
*/
|
|
259
|
+
readonly factoryModules: Map<string, Promise<FactoryModuleScope | undefined>>;
|
|
220
260
|
}
|
|
221
261
|
|
|
222
262
|
/**
|
|
@@ -256,6 +296,7 @@ export function createFoldSession(
|
|
|
256
296
|
buildParams,
|
|
257
297
|
lexiconPackages: new Set(lexicons.map(lexiconPackageName)),
|
|
258
298
|
sandbox,
|
|
299
|
+
factoryModules: new Map(),
|
|
259
300
|
};
|
|
260
301
|
}
|
|
261
302
|
|
|
@@ -392,6 +433,60 @@ function hasExportModifier(node: { modifiers?: ts.NodeArray<ts.ModifierLike> }):
|
|
|
392
433
|
return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
393
434
|
}
|
|
394
435
|
|
|
436
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
437
|
+
// Execution accounting (chant #1023).
|
|
438
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* What the fold pass actually EXECUTED, and what it interpreted instead — the
|
|
442
|
+
* number chant #1023 is measured by, and the one the #1093/#1111
|
|
443
|
+
* execution-boundary report could not state before: `--sandbox` proves
|
|
444
|
+
* *nothing project-owned ran in this process*, but plain `--fold` still
|
|
445
|
+
* invoked composite factories in-process, and nothing counted them.
|
|
446
|
+
*
|
|
447
|
+
* Purely observational. Two integer increments on paths that were already
|
|
448
|
+
* about to perform a dynamic `import()` or parse a module, so it costs
|
|
449
|
+
* nothing measurable and changes no decision.
|
|
450
|
+
*/
|
|
451
|
+
export interface FoldExecutionCounts {
|
|
452
|
+
/**
|
|
453
|
+
* Composite-factory / wrapper calls {@link resolveCallExpression} resolved
|
|
454
|
+
* by importing the defining module and CALLING it in this process.
|
|
455
|
+
*/
|
|
456
|
+
factoryInvocations: number;
|
|
457
|
+
/**
|
|
458
|
+
* Of {@link factoryInvocations}, the ones whose callee came from a PROJECT
|
|
459
|
+
* FILE (a relative/absolute specifier) rather than a lexicon package or
|
|
460
|
+
* chant's own — i.e. the ones that execute project-authored code here. Text
|
|
461
|
+
* only ({@link isProjectFileSpecifier}); no resolution is performed to
|
|
462
|
+
* classify.
|
|
463
|
+
*/
|
|
464
|
+
projectFactoryInvocations: number;
|
|
465
|
+
/**
|
|
466
|
+
* Composite factory bodies {@link interpretCompositeFactory} evaluated
|
|
467
|
+
* statically instead — each one an invocation that did NOT happen.
|
|
468
|
+
*/
|
|
469
|
+
factoryInterpretations: number;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const executionCounts: FoldExecutionCounts = {
|
|
473
|
+
factoryInvocations: 0,
|
|
474
|
+
projectFactoryInvocations: 0,
|
|
475
|
+
factoryInterpretations: 0,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/** A snapshot of {@link FoldExecutionCounts}. Process-wide and monotonic — a caller wanting a per-build figure calls {@link resetFoldExecutionCounts} first. */
|
|
479
|
+
export function foldExecutionCounts(): Readonly<FoldExecutionCounts> {
|
|
480
|
+
return { ...executionCounts };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Zero {@link foldExecutionCounts}, for a caller measuring one build. */
|
|
484
|
+
export function resetFoldExecutionCounts(): void {
|
|
485
|
+
executionCounts.factoryInvocations = 0;
|
|
486
|
+
executionCounts.projectFactoryInvocations = 0;
|
|
487
|
+
executionCounts.factoryInterpretations = 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
395
490
|
/**
|
|
396
491
|
* True for any non-null, non-array object — a real `CompositeInstance`, a
|
|
397
492
|
* real `Declarable`, or (chant #1020) the synthetic plain object a
|
|
@@ -1049,6 +1144,23 @@ interface ResolveCtx {
|
|
|
1049
1144
|
lexiconPackages: ReadonlySet<string>;
|
|
1050
1145
|
/** chant #1093 — see {@link FoldSession.sandbox}. */
|
|
1051
1146
|
sandbox: boolean;
|
|
1147
|
+
/**
|
|
1148
|
+
* chant #1023 — the whole build session, for the two things composite-factory
|
|
1149
|
+
* interpretation needs that a per-file context cannot carry: the
|
|
1150
|
+
* {@link FoldSession.factoryModules} memo (a composite's defining module is
|
|
1151
|
+
* parsed once per BUILD, not once per calling file) and
|
|
1152
|
+
* {@link FoldSession.stack} (so a factory whose module is already being
|
|
1153
|
+
* resolved further up the same chain is a detected cycle, not a hang).
|
|
1154
|
+
*/
|
|
1155
|
+
session: FoldSession;
|
|
1156
|
+
/**
|
|
1157
|
+
* chant #1023 — how many composite factory bodies are being interpreted
|
|
1158
|
+
* around this context, 0 at a file's own top level. A composite that
|
|
1159
|
+
* (directly or through its members) calls itself has no fixpoint and no file
|
|
1160
|
+
* boundary for {@link FoldSession.stack} to notice, so this is what
|
|
1161
|
+
* terminates it — see {@link MAX_INTERPRETATION_DEPTH}.
|
|
1162
|
+
*/
|
|
1163
|
+
interpretDepth: number;
|
|
1052
1164
|
}
|
|
1053
1165
|
|
|
1054
1166
|
/** `{ value }` when `node`'s shape was recognized and resolved (value may itself be `undefined`/`null` — e.g. an optional composite member that wasn't created); `undefined` when the shape isn't one the live resolver understands (a plain literal, etc.) — callers fall back to the original, unchanged handling for that shape. */
|
|
@@ -1127,7 +1239,7 @@ async function resolveLiveValue(node: ts.Expression, ctx: ResolveCtx): Promise<L
|
|
|
1127
1239
|
}
|
|
1128
1240
|
|
|
1129
1241
|
/**
|
|
1130
|
-
* Resolve
|
|
1242
|
+
* Resolve a bare call expression — a composite factory call
|
|
1131
1243
|
* (`SomeComposite({...})`) or a wrapper that takes a composite instance and
|
|
1132
1244
|
* returns one (`propagate(SomeComposite({...}), {...})`). The callee must be
|
|
1133
1245
|
* a plain identifier bound by this file's own `import` (a namespace-import
|
|
@@ -1135,13 +1247,25 @@ async function resolveLiveValue(node: ts.Expression, ctx: ResolveCtx): Promise<L
|
|
|
1135
1247
|
* same file, can't be resolved without running the file — falls back, same
|
|
1136
1248
|
* as an unresolvable resource constructor).
|
|
1137
1249
|
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1250
|
+
* Two ways to get the value, tried in that order:
|
|
1251
|
+
*
|
|
1252
|
+
* 1. **Interpretation** (chant #1023) — the callee is an interpretable
|
|
1253
|
+
* registered `Composite` defined in a PROJECT file, so its body is
|
|
1254
|
+
* evaluated statically and nothing is imported or run. See
|
|
1255
|
+
* {@link resolveInterpretableFactory} for the exact admissible subset.
|
|
1256
|
+
* 2. **Invocation** (chant #1022) — everything else. Each argument is
|
|
1257
|
+
* resolved (recursively, for a nested composite-call/member-access
|
|
1258
|
+
* argument like `propagate`'s first one) or folded (for a plain props
|
|
1259
|
+
* object literal via {@link fold}), the real function is imported and
|
|
1260
|
+
* invoked, and the RESULT is what matters to the caller —
|
|
1261
|
+
* {@link resolveLiveValue}'s callers decide what shape they need (a
|
|
1262
|
+
* `CompositeInstance` for member access, an
|
|
1263
|
+
* `isDeclarable`/`isCompositeInstance` value for a top-level export).
|
|
1264
|
+
* No pre-check verifies the resolved callee is "really" a composite.
|
|
1265
|
+
*
|
|
1266
|
+
* Arm 2 is untouched by #1023, including for a factory arm 1 STARTED and then
|
|
1267
|
+
* declined on: interpretation is all-or-nothing per call, and a decline is not
|
|
1268
|
+
* a fold failure — it lands exactly where the code landed before this existed.
|
|
1145
1269
|
*/
|
|
1146
1270
|
async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx): Promise<unknown> {
|
|
1147
1271
|
// chant #1054 — reuses ../fold/subset's `callExpressionMessage` (the SAME
|
|
@@ -1158,6 +1282,43 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1158
1282
|
throw cheapError(callExpressionMessage(node));
|
|
1159
1283
|
}
|
|
1160
1284
|
|
|
1285
|
+
// chant #1023 — is this callee a composite whose body can be INTERPRETED
|
|
1286
|
+
// rather than run? Purely a static question (does the defining module parse,
|
|
1287
|
+
// does it declare this export as `Composite(<fn>, "<name>")`, is `<fn>`'s
|
|
1288
|
+
// body inside the admissible subset) — no argument has been evaluated yet
|
|
1289
|
+
// and nothing has been imported, so a `undefined` here has cost nothing and
|
|
1290
|
+
// changed nothing.
|
|
1291
|
+
const factory = await resolveInterpretableFactory(binding, ctx);
|
|
1292
|
+
|
|
1293
|
+
if (factory) {
|
|
1294
|
+
const args = await resolveCallArguments(node, calleeName, ctx);
|
|
1295
|
+
const interpreted = await interpretCompositeFactory(factory, args, ctx);
|
|
1296
|
+
if (interpreted) return interpreted.value;
|
|
1297
|
+
// Declined — the body's shape passed but something in it did not resolve
|
|
1298
|
+
// (an identifier the module's own imports don't reach, a constructor
|
|
1299
|
+
// --sandbox refuses). Fall through to arm 2 with the arguments ALREADY
|
|
1300
|
+
// evaluated, so a nested composite-call argument is not built twice.
|
|
1301
|
+
return invokeImportedCallee(node, calleeName, binding, ctx, args);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return invokeImportedCallee(node, calleeName, binding, ctx);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Arm 2 — import the module the callee came from and call it, in this process.
|
|
1309
|
+
* Unchanged from #1022 in both behavior and ORDER (refusal, resolve, import,
|
|
1310
|
+
* callable check, arguments, call); #1023 only factored it out so the
|
|
1311
|
+
* interpretation arm above can fall back into it.
|
|
1312
|
+
*
|
|
1313
|
+
* @param args - Already-evaluated arguments, when the caller has them.
|
|
1314
|
+
*/
|
|
1315
|
+
async function invokeImportedCallee(
|
|
1316
|
+
node: ts.CallExpression,
|
|
1317
|
+
calleeName: string,
|
|
1318
|
+
binding: ImportBinding,
|
|
1319
|
+
ctx: ResolveCtx,
|
|
1320
|
+
args?: readonly unknown[],
|
|
1321
|
+
): Promise<unknown> {
|
|
1161
1322
|
// chant #1093 — THE gap this check exists for. Invoking the callee runs
|
|
1162
1323
|
// project code (the factory body, and its whole module's top level) in the
|
|
1163
1324
|
// CLI's own process; under --sandbox that has to happen in the child
|
|
@@ -1202,21 +1363,43 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1202
1363
|
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a function`);
|
|
1203
1364
|
}
|
|
1204
1365
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1366
|
+
const callArgs = args ?? (await resolveCallArguments(node, calleeName, ctx));
|
|
1367
|
+
|
|
1368
|
+
executionCounts.factoryInvocations += 1;
|
|
1369
|
+
if (isProjectFileSpecifier(binding.specifier)) executionCounts.projectFactoryInvocations += 1;
|
|
1370
|
+
|
|
1371
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(...callArgs);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Resolve a call's arguments to the real values the callee would receive —
|
|
1376
|
+
* factored out of {@link resolveCallExpression} (chant #1023) so the
|
|
1377
|
+
* interpretation and invocation arms consume ONE evaluation of them. Anything
|
|
1378
|
+
* else would evaluate a nested composite-call argument twice and hand the
|
|
1379
|
+
* callee the second instance while the first was already wired into an
|
|
1380
|
+
* `AttrRef`.
|
|
1381
|
+
*
|
|
1382
|
+
* chant #1112 — ONE rule for a registered authoring helper's arguments,
|
|
1383
|
+
* applied at BOTH sites that can invoke one. `reviveHelperCall` (the
|
|
1384
|
+
* nested-value site, #1082) already revives a helper's arguments with
|
|
1385
|
+
* `requireLiveRefs`, because a helper reads THROUGH its ref (`output()`
|
|
1386
|
+
* derefs the `WeakRef` parent) and a look-alike `{__attrRef}` envelope
|
|
1387
|
+
* makes it produce a wrong result rather than none. This site — a
|
|
1388
|
+
* top-level `export const oArn = output(bucket.Arn, "oArn")` — took the
|
|
1389
|
+
* composite-factory rule (`false`) instead, which is right for a factory
|
|
1390
|
+
* (its props keep the envelope, and the serializer's own walker resolves
|
|
1391
|
+
* it) and wrong for a helper. It went unnoticed while the resulting
|
|
1392
|
+
* `LexiconOutput` was being discarded anyway; with the export namespace
|
|
1393
|
+
* now collected in full, a same-file `output(...)` would reach the
|
|
1394
|
+
* serializer holding an inert envelope where the run path has a real
|
|
1395
|
+
* reference. Rejected here instead, which falls the file back to run —
|
|
1396
|
+
* absent output, never a wrong one.
|
|
1397
|
+
*/
|
|
1398
|
+
async function resolveCallArguments(
|
|
1399
|
+
node: ts.CallExpression,
|
|
1400
|
+
calleeName: string,
|
|
1401
|
+
ctx: ResolveCtx,
|
|
1402
|
+
): Promise<unknown[]> {
|
|
1220
1403
|
const helperArgs = isFoldableHelperName(calleeName);
|
|
1221
1404
|
const args: unknown[] = [];
|
|
1222
1405
|
for (const argNode of node.arguments) {
|
|
@@ -1231,8 +1414,767 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1231
1414
|
: await reviveFoldedValue(fold(argNode, ctx.consts, ctx.intrinsics, ctx.externals), ctx, helperArgs),
|
|
1232
1415
|
);
|
|
1233
1416
|
}
|
|
1417
|
+
return args;
|
|
1418
|
+
}
|
|
1234
1419
|
|
|
1235
|
-
|
|
1420
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1421
|
+
// Composite factory interpretation (chant #1023, epic #1019 Phase 5).
|
|
1422
|
+
//
|
|
1423
|
+
// THE CONTRACT — the admissible factory subset, in ../fold/subset.ts's style.
|
|
1424
|
+
//
|
|
1425
|
+
// A composite is a factory function, and #1022 got its value the only way it
|
|
1426
|
+
// could: by importing the defining module and CALLING it. That is the last
|
|
1427
|
+
// place `chant build --fold` executes project-authored code in the CLI's own
|
|
1428
|
+
// process (chant #1093), and the reason `--sandbox` has to demote such a file
|
|
1429
|
+
// to the sandboxed child instead of folding it (chant #1111). This section
|
|
1430
|
+
// removes the call for the factories whose bodies can be evaluated instead.
|
|
1431
|
+
//
|
|
1432
|
+
// A call is interpreted when ALL of the following hold. Every one of them is
|
|
1433
|
+
// checked BEFORE any argument is evaluated or anything is constructed, so a
|
|
1434
|
+
// factory outside the subset costs a parse and nothing else:
|
|
1435
|
+
//
|
|
1436
|
+
// 1. **The callee is bound by an `import` from a PROJECT FILE** (a
|
|
1437
|
+
// relative/absolute specifier). A lexicon-package composite is
|
|
1438
|
+
// deliberately NOT interpreted, for two independent reasons: an installed
|
|
1439
|
+
// lexicon ships compiled JS, so whether its factory bodies were
|
|
1440
|
+
// interpretable would depend on whether the package happened to ship
|
|
1441
|
+
// `.ts` — fold coverage must not vary with a dependency's build shape —
|
|
1442
|
+
// and a lexicon package is on {@link isTrustedExecutableBinding}'s
|
|
1443
|
+
// allowlist already, so calling it is exactly as safe under `--sandbox` as
|
|
1444
|
+
// the `loadPlugins` import the CLI performed before discovery began. There
|
|
1445
|
+
// is nothing to buy and a build-shape dependency to lose.
|
|
1446
|
+
// 2. **The defining module declares that export as `Composite(<fn>, "<name>")`**
|
|
1447
|
+
// — a top-level `export const <name> = Composite(...)`, where `Composite`
|
|
1448
|
+
// is bound in THAT module to an import of chant's own
|
|
1449
|
+
// ({@link isChantOwnedHelperBinding}, the same provenance question #1082
|
|
1450
|
+
// asks of an authoring helper, and the same answer). This is #1023's
|
|
1451
|
+
// "recognize the callee is a registered Composite": a bare call to
|
|
1452
|
+
// anything else — a project helper function, a `withDefaults(...)`
|
|
1453
|
+
// wrapper, a re-exported binding — is not recognized and is not
|
|
1454
|
+
// interpreted.
|
|
1455
|
+
// 3. **`<fn>` is an arrow/function expression taking at most one parameter**,
|
|
1456
|
+
// bound as a plain identifier or a simple object binding pattern (no
|
|
1457
|
+
// default, rest, or nested pattern).
|
|
1458
|
+
// 4. **Its body is a single expression, or a block of `const` declarations
|
|
1459
|
+
// followed by one `return`** — and nothing else. No `if`, no `throw`, no
|
|
1460
|
+
// loop, no `let`/`var`, no nested function declaration, no bare expression
|
|
1461
|
+
// statement. This is the line loomster's `composites/*.ts` fall outside
|
|
1462
|
+
// (module-level `buildXxx()` helpers with `if`/`throw` and `.map()`), and
|
|
1463
|
+
// they are meant to: they keep invoking, exactly as before.
|
|
1464
|
+
// 5. **Every expression in it is inside the fold subset, extended with the
|
|
1465
|
+
// two things a factory body exists to do**: `new Type(...)` in ANY value
|
|
1466
|
+
// position (a member, a nested property object, an array element), and a
|
|
1467
|
+
// call through a bare identifier. Both are shape-admissible here and
|
|
1468
|
+
// resolution-checked at evaluation, the same permissive-shape /
|
|
1469
|
+
// strict-evaluation split ../fold/subset.ts documents for identifier
|
|
1470
|
+
// binding and helper provenance.
|
|
1471
|
+
//
|
|
1472
|
+
// Everything else is out: a computed key, a method call (`naming.name(...)` —
|
|
1473
|
+
// the callee is a property access, not an identifier), an array method, an
|
|
1474
|
+
// operator `fold()` doesn't implement, `await`, a class expression, a template
|
|
1475
|
+
// with a computed tag.
|
|
1476
|
+
//
|
|
1477
|
+
// WHY THIS IS NOT IN ../fold/subset.ts. That module is the shared, shape-only
|
|
1478
|
+
// predicate `fold()` and EVL both read, and its own contract is that it may
|
|
1479
|
+
// only ever be PERMISSIVE relative to `fold()`, never stricter. Admissibility
|
|
1480
|
+
// here is not a shape question at all: rules 1 and 2 need the module graph —
|
|
1481
|
+
// which file a name came from, and what that file declares — exactly like
|
|
1482
|
+
// #1082's authoring-helper provenance, which subset.ts documents (point 2b)
|
|
1483
|
+
// as deliberately out for the same reason. Putting rules 3-5 there alone would
|
|
1484
|
+
// describe a subset no caller could act on without also answering 1 and 2, and
|
|
1485
|
+
// putting all five there would mean teaching a syntax-only lint rule to
|
|
1486
|
+
// resolve imports. The shape half stays checkable by anything that wants it —
|
|
1487
|
+
// {@link findFactorySubsetViolation} takes a lone `ts` node — but the shared
|
|
1488
|
+
// predicate is not made stricter, or wider, by this issue.
|
|
1489
|
+
//
|
|
1490
|
+
// WHAT IS PRESERVED. Interpretation is not a second construction path:
|
|
1491
|
+
//
|
|
1492
|
+
// - **The instances are real, and made by the lexicon's own constructors.**
|
|
1493
|
+
// A `new Role({...})` in the body resolves `Role` through the DEFINING
|
|
1494
|
+
// module's imports and calls that class, the same class the run path would
|
|
1495
|
+
// have called from the same resolved module path.
|
|
1496
|
+
// - **Sibling references are live, not symbolic.** `role.Arn` reads the
|
|
1497
|
+
// attribute off the `Role` instance this interpretation just built, so it
|
|
1498
|
+
// is a genuine `AttrRef` wired to a genuine parent — identical to running
|
|
1499
|
+
// the factory, and strictly better than a top-level fold's `{__attrRef}`
|
|
1500
|
+
// envelope (an intrinsic that inspects its argument, `Sub`/`Ref`, gets what
|
|
1501
|
+
// it expects here).
|
|
1502
|
+
// - **The instance is assembled by `Composite()` itself.** The members go
|
|
1503
|
+
// back through chant's own {@link Composite} (../composite.ts) rather than
|
|
1504
|
+
// through a hand-built look-alike, so member validation, the non-enumerable
|
|
1505
|
+
// `members`/`_definition` layout `propagate()` and `expandComposite()`
|
|
1506
|
+
// depend on, and provenance stamping are the run path's, by construction
|
|
1507
|
+
// and not by resemblance.
|
|
1508
|
+
// - **`propagate()` still works, untouched.** It is a chant-owned import, so
|
|
1509
|
+
// `propagate(SomeComposite({...}), {...})` invokes the real `propagate` on
|
|
1510
|
+
// the interpreted instance and mutates it in place exactly as it does a run
|
|
1511
|
+
// one — which is why instance identity has to be a real `CompositeInstance`
|
|
1512
|
+
// and not a copy (chant #1097).
|
|
1513
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* A backstop for a composite that calls itself, directly or through a member.
|
|
1517
|
+
* {@link FoldSession.stack} cannot see it — the recursion happens entirely
|
|
1518
|
+
* inside one module's source, crossing no file boundary — so this is what
|
|
1519
|
+
* terminates it. Deliberately small: real composite nesting is 2-3 deep, and a
|
|
1520
|
+
* chain past this is a bug, not a design.
|
|
1521
|
+
*/
|
|
1522
|
+
const MAX_INTERPRETATION_DEPTH = 16;
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* The static scope of a module that DEFINES composites — everything
|
|
1526
|
+
* interpreting one of its factory bodies needs, computed once per module per
|
|
1527
|
+
* build (see {@link FoldSession.factoryModules}).
|
|
1528
|
+
*
|
|
1529
|
+
* Note what is NOT here: the module's exported VALUES. Interpreting a factory
|
|
1530
|
+
* never needs them, which is the whole point — a module can define a perfectly
|
|
1531
|
+
* interpretable composite and still be unfoldable as a module (an exported
|
|
1532
|
+
* function declaration alongside it, say), and those two facts are
|
|
1533
|
+
* independent.
|
|
1534
|
+
*/
|
|
1535
|
+
interface FactoryModuleScope {
|
|
1536
|
+
file: string;
|
|
1537
|
+
sourceFile: ts.SourceFile;
|
|
1538
|
+
/**
|
|
1539
|
+
* The module's top-level `const`s, MINUS every one that resolves to a
|
|
1540
|
+
* `new Type(...)` resource.
|
|
1541
|
+
*
|
|
1542
|
+
* The exclusion is the load-bearing part. `fold()` turns a property access
|
|
1543
|
+
* on a resource-valued const into a symbolic `{__attrRef, entity: "<the
|
|
1544
|
+
* const's name>"}` — a name resolved much later, against the entity table of
|
|
1545
|
+
* the file being COLLECTED, which is the calling file and not this one. A
|
|
1546
|
+
* module-level resource shared by every call of a factory is also a
|
|
1547
|
+
* singleton whose identity the run path shares and interpretation would not.
|
|
1548
|
+
* Dropping those consts makes any reference to one an ordinary "unresolved
|
|
1549
|
+
* identifier" failure, which declines the interpretation and invokes
|
|
1550
|
+
* instead — the answer that is right on both counts.
|
|
1551
|
+
*/
|
|
1552
|
+
consts: Map<string, ts.Expression>;
|
|
1553
|
+
imports: Map<string, ImportBinding>;
|
|
1554
|
+
namespaceImports: Map<string, NamespaceImportBinding>;
|
|
1555
|
+
/**
|
|
1556
|
+
* This module's own imports, resolved to their real cross-file values (see
|
|
1557
|
+
* {@link ResolveCtx.externals}) — LAZILY, and memoized here once built.
|
|
1558
|
+
*
|
|
1559
|
+
* Laziness is not an optimization detail, it is what keeps this issue from
|
|
1560
|
+
* paying #1020's cost all over again. `resolveCallExpression` reaches
|
|
1561
|
+
* {@link resolveInterpretableFactory} for EVERY call through a project-file
|
|
1562
|
+
* import, the overwhelming majority of which are not composites at all. Only
|
|
1563
|
+
* the parse is spent finding that out; resolving a module's whole import
|
|
1564
|
+
* graph — which recursively folds every project file it names — is spent
|
|
1565
|
+
* only by a call that is actually about to be interpreted.
|
|
1566
|
+
*/
|
|
1567
|
+
resolved?: Promise<{ externals: Map<string, unknown>; failures: Map<string, string> }>;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/** An admissible composite factory: where it lives, what it is called, and the function to interpret. */
|
|
1571
|
+
interface InterpretableFactory {
|
|
1572
|
+
scope: FactoryModuleScope;
|
|
1573
|
+
fn: ts.ArrowFunction | ts.FunctionExpression;
|
|
1574
|
+
/** `Composite()`'s second argument, or `"anonymous"` when it has none — matching {@link Composite}'s own default. */
|
|
1575
|
+
compositeName: string;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Parse a composite-defining module into its static scope, memoized per build.
|
|
1580
|
+
* Returns `undefined` when the file can't be read or parsed — a decline, never
|
|
1581
|
+
* a throw: the caller falls through to the invocation arm, which will produce
|
|
1582
|
+
* its own (identical, pre-#1023) error if the module is genuinely broken.
|
|
1583
|
+
*
|
|
1584
|
+
* Reads and parses only. Nothing is imported, nothing is executed, and the
|
|
1585
|
+
* module's own imports are not resolved yet — see
|
|
1586
|
+
* {@link FactoryModuleScope.resolved}.
|
|
1587
|
+
*/
|
|
1588
|
+
function readFactoryModule(modulePath: string, session: FoldSession): Promise<FactoryModuleScope | undefined> {
|
|
1589
|
+
const cached = session.factoryModules.get(modulePath);
|
|
1590
|
+
if (cached) return cached;
|
|
1591
|
+
const promise = readFactoryModuleCore(modulePath);
|
|
1592
|
+
session.factoryModules.set(modulePath, promise);
|
|
1593
|
+
return promise;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
async function readFactoryModuleCore(modulePath: string): Promise<FactoryModuleScope | undefined> {
|
|
1597
|
+
let sourceFile: ts.SourceFile;
|
|
1598
|
+
try {
|
|
1599
|
+
const source = await readFile(modulePath, "utf-8");
|
|
1600
|
+
sourceFile = ts.createSourceFile(modulePath, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
1601
|
+
} catch {
|
|
1602
|
+
// Not a readable TypeScript source — an installed package's compiled
|
|
1603
|
+
// entry point reached through a path specifier, a missing file. Decline.
|
|
1604
|
+
return undefined;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const collected = collectImports(sourceFile);
|
|
1608
|
+
const consts = collectConsts(sourceFile);
|
|
1609
|
+
for (const [name] of [...consts]) {
|
|
1610
|
+
if (constResolvesToResource(consts, name, new Set())) consts.delete(name);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
return {
|
|
1614
|
+
file: modulePath,
|
|
1615
|
+
sourceFile,
|
|
1616
|
+
consts,
|
|
1617
|
+
imports: collected.named,
|
|
1618
|
+
namespaceImports: collected.namespaces,
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Resolve a composite-defining module's own imports, once per module per
|
|
1624
|
+
* build. Guarded by {@link FoldSession.stack}, exactly like
|
|
1625
|
+
* {@link foldFileMemoized}: a module already being resolved further up the
|
|
1626
|
+
* same chain is a genuine cycle, and gets an empty scope (every identifier
|
|
1627
|
+
* then reads as unresolved, which declines the interpretation) rather than a
|
|
1628
|
+
* re-entry.
|
|
1629
|
+
*/
|
|
1630
|
+
function factoryModuleScopeResolved(
|
|
1631
|
+
scope: FactoryModuleScope,
|
|
1632
|
+
session: FoldSession,
|
|
1633
|
+
): Promise<{ externals: Map<string, unknown>; failures: Map<string, string> }> {
|
|
1634
|
+
if (scope.resolved) return scope.resolved;
|
|
1635
|
+
if (session.stack.includes(scope.file) || session.stack.length >= MAX_RESOLUTION_DEPTH) {
|
|
1636
|
+
return Promise.resolve({ externals: new Map(), failures: new Map() });
|
|
1637
|
+
}
|
|
1638
|
+
session.stack.push(scope.file);
|
|
1639
|
+
scope.resolved = buildExternals(scope.file, scope.imports, scope.namespaceImports, session).finally(() => {
|
|
1640
|
+
const idx = session.stack.lastIndexOf(scope.file);
|
|
1641
|
+
if (idx !== -1) session.stack.splice(idx, 1);
|
|
1642
|
+
});
|
|
1643
|
+
return scope.resolved;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* True when a module-level `const` is (transitively) bound to a
|
|
1648
|
+
* `new Type(...)`. Mirrors `fold()`'s own `resolvesToResource`, but follows an
|
|
1649
|
+
* identifier chain (`const a = new T(); const b = a;`) so aliasing can't smuggle
|
|
1650
|
+
* a module-level resource into a factory body — see
|
|
1651
|
+
* {@link FactoryModuleScope.consts}.
|
|
1652
|
+
*/
|
|
1653
|
+
function constResolvesToResource(
|
|
1654
|
+
consts: Map<string, ts.Expression>,
|
|
1655
|
+
name: string,
|
|
1656
|
+
seen: Set<string>,
|
|
1657
|
+
): boolean {
|
|
1658
|
+
if (seen.has(name)) return false;
|
|
1659
|
+
seen.add(name);
|
|
1660
|
+
const init = consts.get(name);
|
|
1661
|
+
if (init === undefined) return false;
|
|
1662
|
+
if (ts.isNewExpression(init)) return true;
|
|
1663
|
+
if (ts.isIdentifier(init)) return constResolvesToResource(consts, init.text, seen);
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Find `export const <exportName> = Composite(<fn>, "<name>")` at the top level
|
|
1669
|
+
* of `scope`'s module, verifying that `Composite` really is chant's own in that
|
|
1670
|
+
* module. Returns `undefined` for every other shape — see rule 2 of the
|
|
1671
|
+
* contract above.
|
|
1672
|
+
*/
|
|
1673
|
+
function findCompositeDefinition(
|
|
1674
|
+
scope: FactoryModuleScope,
|
|
1675
|
+
exportName: string,
|
|
1676
|
+
ctx: ResolveCtx,
|
|
1677
|
+
): { fn: ts.ArrowFunction | ts.FunctionExpression; compositeName: string } | undefined {
|
|
1678
|
+
for (const statement of scope.sourceFile.statements) {
|
|
1679
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
1680
|
+
if (!hasExportModifier(statement)) continue;
|
|
1681
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
1682
|
+
|
|
1683
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1684
|
+
if (!ts.isIdentifier(decl.name) || decl.name.text !== exportName) continue;
|
|
1685
|
+
const init = decl.initializer;
|
|
1686
|
+
if (!init || !ts.isCallExpression(init) || !ts.isIdentifier(init.expression)) return undefined;
|
|
1687
|
+
|
|
1688
|
+
// `Composite` must be bound, in THIS module, to an import of chant's
|
|
1689
|
+
// own — the identical provenance question #1082 asks of an authoring
|
|
1690
|
+
// helper, answered by the identical predicate. A project-local
|
|
1691
|
+
// `function Composite(...)` shadowing the name is not chant's, and a
|
|
1692
|
+
// call to it is not a registered composite.
|
|
1693
|
+
const compositeBinding = scope.imports.get(init.expression.text);
|
|
1694
|
+
if (!compositeBinding || compositeBinding.imported !== "Composite") return undefined;
|
|
1695
|
+
if (!isChantOwnedHelperBinding(compositeBinding, { ...ctx, file: scope.file })) return undefined;
|
|
1696
|
+
|
|
1697
|
+
const [fnArg, nameArg] = init.arguments;
|
|
1698
|
+
if (!fnArg || (!ts.isArrowFunction(fnArg) && !ts.isFunctionExpression(fnArg))) return undefined;
|
|
1699
|
+
// `Composite()`'s own default when the name is omitted (../composite.ts).
|
|
1700
|
+
// COR017 requires the literal in practice; anything that is not a plain
|
|
1701
|
+
// string literal is not something to guess at.
|
|
1702
|
+
if (nameArg !== undefined && !ts.isStringLiteral(nameArg)) return undefined;
|
|
1703
|
+
return { fn: fnArg, compositeName: nameArg ? nameArg.text : "anonymous" };
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Decide whether `binding`'s callee is an interpretable composite — rules 1-5
|
|
1711
|
+
* of the contract above, in that order, with no evaluation and no import.
|
|
1712
|
+
* `undefined` means "not interpretable", never "broken".
|
|
1713
|
+
*/
|
|
1714
|
+
async function resolveInterpretableFactory(
|
|
1715
|
+
binding: ImportBinding,
|
|
1716
|
+
ctx: ResolveCtx,
|
|
1717
|
+
): Promise<InterpretableFactory | undefined> {
|
|
1718
|
+
// Rule 1 — project files only. A text check; no resolution performed for a
|
|
1719
|
+
// bare specifier, so this costs nothing for the (common) lexicon case.
|
|
1720
|
+
if (!isProjectFileSpecifier(binding.specifier)) return undefined;
|
|
1721
|
+
if (ctx.interpretDepth >= MAX_INTERPRETATION_DEPTH) return undefined;
|
|
1722
|
+
|
|
1723
|
+
let modulePath: string;
|
|
1724
|
+
try {
|
|
1725
|
+
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
1726
|
+
} catch {
|
|
1727
|
+
return undefined;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const scope = await readFactoryModule(modulePath, ctx.session);
|
|
1731
|
+
if (!scope) return undefined;
|
|
1732
|
+
|
|
1733
|
+
// Rule 2.
|
|
1734
|
+
const definition = findCompositeDefinition(scope, binding.imported, ctx);
|
|
1735
|
+
if (!definition) return undefined;
|
|
1736
|
+
|
|
1737
|
+
// Rules 3-5.
|
|
1738
|
+
if (findFactorySubsetViolation(definition.fn) !== undefined) return undefined;
|
|
1739
|
+
|
|
1740
|
+
return { scope, fn: definition.fn, compositeName: definition.compositeName };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* The SHAPE half of the contract (rules 3-5) — a lone `ts` node in, a reason
|
|
1745
|
+
* string out, or `undefined` when the factory is admissible. Deliberately
|
|
1746
|
+
* takes nothing but the node: it is the half a caller with no module graph
|
|
1747
|
+
* could evaluate, and keeping it separable is what lets the doc above claim
|
|
1748
|
+
* the shape rules are checkable without the provenance ones.
|
|
1749
|
+
*/
|
|
1750
|
+
export function findFactorySubsetViolation(fn: ts.ArrowFunction | ts.FunctionExpression): string | undefined {
|
|
1751
|
+
// Rule 3 — at most one parameter, bound plainly.
|
|
1752
|
+
if (fn.parameters.length > 1) return "a composite factory takes a single props parameter";
|
|
1753
|
+
const param = fn.parameters[0];
|
|
1754
|
+
if (param) {
|
|
1755
|
+
if (param.dotDotDotToken) return "a rest parameter is not interpretable";
|
|
1756
|
+
if (param.initializer) return "a defaulted parameter is not interpretable";
|
|
1757
|
+
if (ts.isObjectBindingPattern(param.name)) {
|
|
1758
|
+
for (const el of param.name.elements) {
|
|
1759
|
+
if (bindingElementPropKey(el) === undefined) {
|
|
1760
|
+
return "a destructured props parameter with a rest, default, or nested element is not interpretable";
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
} else if (!ts.isIdentifier(param.name)) {
|
|
1764
|
+
return "an array-destructured props parameter is not interpretable";
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Rule 4 — a concise expression body, or `const`s then one `return`.
|
|
1769
|
+
if (!ts.isBlock(fn.body)) return checkFactoryExpression(fn.body);
|
|
1770
|
+
|
|
1771
|
+
const statements = fn.body.statements;
|
|
1772
|
+
if (statements.length === 0) return "an empty composite factory body has no members to interpret";
|
|
1773
|
+
for (let i = 0; i < statements.length; i += 1) {
|
|
1774
|
+
const statement = statements[i];
|
|
1775
|
+
const last = i === statements.length - 1;
|
|
1776
|
+
|
|
1777
|
+
if (ts.isReturnStatement(statement)) {
|
|
1778
|
+
if (!last) return "an early `return` is not interpretable";
|
|
1779
|
+
if (!statement.expression) return "a composite factory must return its members";
|
|
1780
|
+
const violation = checkFactoryExpression(statement.expression);
|
|
1781
|
+
if (violation) return violation;
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
if (last) return "a composite factory body must end in `return`";
|
|
1785
|
+
|
|
1786
|
+
if (!ts.isVariableStatement(statement)) {
|
|
1787
|
+
return `\`${ts.SyntaxKind[statement.kind]}\` in a composite factory body is not interpretable`;
|
|
1788
|
+
}
|
|
1789
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
1790
|
+
return "`let`/`var` in a composite factory body is not interpretable";
|
|
1791
|
+
}
|
|
1792
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1793
|
+
if (!decl.initializer) return "an uninitialized `const` in a composite factory body is not interpretable";
|
|
1794
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
1795
|
+
for (const el of decl.name.elements) {
|
|
1796
|
+
if (bindingElementPropKey(el) === undefined) {
|
|
1797
|
+
return "a destructured `const` with a rest, default, or nested element is not interpretable";
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} else if (!ts.isIdentifier(decl.name)) {
|
|
1801
|
+
return "an array-destructured `const` is not interpretable";
|
|
1802
|
+
}
|
|
1803
|
+
const violation = checkFactoryExpression(decl.initializer);
|
|
1804
|
+
if (violation) return violation;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return undefined;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Rule 5 — the fold subset, extended with `new Type(...)` in any value
|
|
1812
|
+
* position and a call through a bare identifier.
|
|
1813
|
+
*
|
|
1814
|
+
* Shape only, in ../fold/subset.ts's sense: whether a `new`'s constructor or a
|
|
1815
|
+
* call's callee actually resolves to something invocable is settled at
|
|
1816
|
+
* evaluation, where the module graph is available. Every operator/key/member
|
|
1817
|
+
* rule is read from ../fold/subset.ts's own exported sets rather than
|
|
1818
|
+
* re-listed, so widening `fold()` widens this in the same commit.
|
|
1819
|
+
*/
|
|
1820
|
+
function checkFactoryExpression(node: ts.Expression): string | undefined {
|
|
1821
|
+
if (
|
|
1822
|
+
ts.isParenthesizedExpression(node) ||
|
|
1823
|
+
ts.isAsExpression(node) ||
|
|
1824
|
+
ts.isSatisfiesExpression(node) ||
|
|
1825
|
+
ts.isNonNullExpression(node) ||
|
|
1826
|
+
ts.isTypeAssertionExpression(node)
|
|
1827
|
+
) {
|
|
1828
|
+
return checkFactoryExpression(node.expression);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (
|
|
1832
|
+
ts.isStringLiteral(node) ||
|
|
1833
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
1834
|
+
ts.isNumericLiteral(node) ||
|
|
1835
|
+
ts.isIdentifier(node) ||
|
|
1836
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
1837
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
1838
|
+
node.kind === ts.SyntaxKind.NullKeyword
|
|
1839
|
+
) {
|
|
1840
|
+
return undefined;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (ts.isNewExpression(node) || ts.isCallExpression(node)) {
|
|
1844
|
+
// A bare-identifier callee only — `ns.Foo(...)`, `arr.map(...)` and
|
|
1845
|
+
// `props.factory(...)` are all property accesses and all stay out, which
|
|
1846
|
+
// is what keeps a data transform (chant EVL010) from sneaking in.
|
|
1847
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
1848
|
+
return ts.isNewExpression(node)
|
|
1849
|
+
? `\`new ${briefNodeText(node.expression)}(...)\` needs a plain imported constructor to interpret`
|
|
1850
|
+
: callExpressionMessage(node);
|
|
1851
|
+
}
|
|
1852
|
+
for (const arg of node.arguments ?? []) {
|
|
1853
|
+
const violation = checkFactoryExpression(arg);
|
|
1854
|
+
if (violation) return violation;
|
|
1855
|
+
}
|
|
1856
|
+
return undefined;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
1860
|
+
if (!ts.isIdentifier(node.tag)) return unsupportedExpressionMessage(node);
|
|
1861
|
+
if (ts.isNoSubstitutionTemplateLiteral(node.template)) return undefined;
|
|
1862
|
+
for (const span of node.template.templateSpans) {
|
|
1863
|
+
const violation = checkFactoryExpression(span.expression);
|
|
1864
|
+
if (violation) return violation;
|
|
1865
|
+
}
|
|
1866
|
+
return undefined;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (ts.isTemplateExpression(node)) {
|
|
1870
|
+
for (const span of node.templateSpans) {
|
|
1871
|
+
const violation = checkFactoryExpression(span.expression);
|
|
1872
|
+
if (violation) return violation;
|
|
1873
|
+
}
|
|
1874
|
+
return undefined;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1878
|
+
for (const prop of node.properties) {
|
|
1879
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
1880
|
+
if (!isLiteralPropertyNameNode(prop.name)) return computedPropertyNameMessage(prop.name);
|
|
1881
|
+
const violation = checkFactoryExpression(prop.initializer);
|
|
1882
|
+
if (violation) return violation;
|
|
1883
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1884
|
+
if (prop.objectAssignmentInitializer) return UNSUPPORTED_OBJECT_MEMBER_MESSAGE;
|
|
1885
|
+
} else if (ts.isSpreadAssignment(prop)) {
|
|
1886
|
+
const violation = checkFactoryExpression(prop.expression);
|
|
1887
|
+
if (violation) return violation;
|
|
1888
|
+
} else {
|
|
1889
|
+
return UNSUPPORTED_OBJECT_MEMBER_MESSAGE;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return undefined;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1896
|
+
for (const el of node.elements) {
|
|
1897
|
+
const violation = checkFactoryExpression(ts.isSpreadElement(el) ? el.expression : el);
|
|
1898
|
+
if (violation) return violation;
|
|
1899
|
+
}
|
|
1900
|
+
return undefined;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (ts.isPropertyAccessExpression(node)) return checkFactoryExpression(node.expression);
|
|
1904
|
+
|
|
1905
|
+
if (ts.isElementAccessExpression(node)) {
|
|
1906
|
+
if (!isLiteralElementKey(node.argumentExpression)) {
|
|
1907
|
+
return `dynamic element access [${briefNodeText(node.argumentExpression)}] is not interpretable`;
|
|
1908
|
+
}
|
|
1909
|
+
return checkFactoryExpression(node.expression);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
1913
|
+
if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) return UNSUPPORTED_UNARY_MESSAGE;
|
|
1914
|
+
return checkFactoryExpression(node.operand);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (ts.isBinaryExpression(node)) {
|
|
1918
|
+
if (!SUPPORTED_BINARY_OPERATORS.has(node.operatorToken.kind)) {
|
|
1919
|
+
return unsupportedBinaryMessage(node.operatorToken.kind);
|
|
1920
|
+
}
|
|
1921
|
+
return checkFactoryExpression(node.left) ?? checkFactoryExpression(node.right);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if (ts.isConditionalExpression(node)) {
|
|
1925
|
+
return (
|
|
1926
|
+
checkFactoryExpression(node.condition) ??
|
|
1927
|
+
checkFactoryExpression(node.whenTrue) ??
|
|
1928
|
+
checkFactoryExpression(node.whenFalse)
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return unsupportedExpressionMessage(node);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/** `isLiteralPropertyName` narrowed to the node type {@link propName} accepts, without importing the type predicate's generic form twice. */
|
|
1936
|
+
function isLiteralPropertyNameNode(node: ts.PropertyName): boolean {
|
|
1937
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/**
|
|
1941
|
+
* Interpret an admissible factory's body against `args`, and assemble the
|
|
1942
|
+
* result through chant's own {@link Composite}.
|
|
1943
|
+
*
|
|
1944
|
+
* Returns `undefined` — a DECLINE, not a failure — when anything in the body
|
|
1945
|
+
* fails to resolve (an identifier the defining module's imports don't reach, a
|
|
1946
|
+
* constructor `--sandbox` refuses, a nested call that isn't a composite). The
|
|
1947
|
+
* caller then invokes for real, landing exactly where it landed before #1023.
|
|
1948
|
+
* That asymmetry is deliberate: interpretation may only ever REMOVE an
|
|
1949
|
+
* execution, never introduce a fold failure that wasn't there.
|
|
1950
|
+
*/
|
|
1951
|
+
async function interpretCompositeFactory(
|
|
1952
|
+
factory: InterpretableFactory,
|
|
1953
|
+
args: readonly unknown[],
|
|
1954
|
+
ctx: ResolveCtx,
|
|
1955
|
+
): Promise<{ value: unknown } | undefined> {
|
|
1956
|
+
// A `Composite()` factory takes exactly one props argument. Anything else at
|
|
1957
|
+
// the call site means the callee is being used as something this doesn't
|
|
1958
|
+
// model.
|
|
1959
|
+
if (args.length > 1) return undefined;
|
|
1960
|
+
|
|
1961
|
+
const { scope, fn } = factory;
|
|
1962
|
+
const resolved = await factoryModuleScopeResolved(scope, ctx.session);
|
|
1963
|
+
// The defining module's own scope, with the factory's locals layered on top.
|
|
1964
|
+
// `locals` is deliberately EMPTY: `resolveLiveValue`'s local-binding branch
|
|
1965
|
+
// exists to re-navigate a file's own top-level composite spine, which is not
|
|
1966
|
+
// what a name inside a factory body means. Body bindings live in `externals`
|
|
1967
|
+
// instead, where an identifier resolves to the value already computed for it
|
|
1968
|
+
// — which is what makes `role.Arn` a live `AttrRef` on the real instance.
|
|
1969
|
+
const consts = new Map(scope.consts);
|
|
1970
|
+
const externals = new Map(resolved.externals);
|
|
1971
|
+
const bodyCtx: ResolveCtx = {
|
|
1972
|
+
file: scope.file,
|
|
1973
|
+
consts,
|
|
1974
|
+
locals: new Map(),
|
|
1975
|
+
imports: scope.imports,
|
|
1976
|
+
namespaceImports: scope.namespaceImports,
|
|
1977
|
+
memo: new Map(),
|
|
1978
|
+
intrinsics: ctx.intrinsics,
|
|
1979
|
+
externals,
|
|
1980
|
+
crossFileFailures: resolved.failures,
|
|
1981
|
+
importCache: ctx.importCache,
|
|
1982
|
+
resolvePathCache: ctx.resolvePathCache,
|
|
1983
|
+
lexiconPackages: ctx.lexiconPackages,
|
|
1984
|
+
sandbox: ctx.sandbox,
|
|
1985
|
+
session: ctx.session,
|
|
1986
|
+
interpretDepth: ctx.interpretDepth + 1,
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
const bind = (name: string, value: unknown): void => {
|
|
1990
|
+
// A body binding SHADOWS a module-level const of the same name, so the
|
|
1991
|
+
// const has to go — `fold()` consults `consts` before `externals`.
|
|
1992
|
+
consts.delete(name);
|
|
1993
|
+
externals.set(name, value);
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
try {
|
|
1997
|
+
const param = fn.parameters[0];
|
|
1998
|
+
if (param) {
|
|
1999
|
+
const props = args[0];
|
|
2000
|
+
if (ts.isIdentifier(param.name)) {
|
|
2001
|
+
bind(param.name.text, props);
|
|
2002
|
+
} else if (ts.isObjectBindingPattern(param.name)) {
|
|
2003
|
+
if (!isIndexableObject(props)) return undefined;
|
|
2004
|
+
for (const el of param.name.elements) {
|
|
2005
|
+
const key = bindingElementPropKey(el);
|
|
2006
|
+
if (key === undefined) return undefined;
|
|
2007
|
+
bind(el.name.getText(), (props as Record<string, unknown>)[key]);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const members = await interpretFactoryBody(fn, bodyCtx, bind);
|
|
2013
|
+
if (!isIndexableObject(members)) return undefined;
|
|
2014
|
+
|
|
2015
|
+
// Assembled by chant's own `Composite()` — see the contract's "what is
|
|
2016
|
+
// preserved" note. One definition object per interpreted call (rather than
|
|
2017
|
+
// one per module, as the run path has) is the single visible difference:
|
|
2018
|
+
// `_id` is a fresh symbol, which nothing outside `CompositeRegistry` — used
|
|
2019
|
+
// only by tests — reads.
|
|
2020
|
+
const definition = Composite<void, CompositeMembers>(() => members as CompositeMembers, factory.compositeName);
|
|
2021
|
+
const instance = definition();
|
|
2022
|
+
executionCounts.factoryInterpretations += 1;
|
|
2023
|
+
return { value: instance };
|
|
2024
|
+
} catch {
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/** Evaluate an admissible factory body's statements in order, binding each `const`, and return the `return` expression's value. */
|
|
2030
|
+
async function interpretFactoryBody(
|
|
2031
|
+
fn: ts.ArrowFunction | ts.FunctionExpression,
|
|
2032
|
+
ctx: ResolveCtx,
|
|
2033
|
+
bind: (name: string, value: unknown) => void,
|
|
2034
|
+
): Promise<unknown> {
|
|
2035
|
+
if (!ts.isBlock(fn.body)) return interpretExpression(fn.body, ctx);
|
|
2036
|
+
|
|
2037
|
+
for (const statement of fn.body.statements) {
|
|
2038
|
+
if (ts.isReturnStatement(statement)) {
|
|
2039
|
+
// `findFactorySubsetViolation` has already established this is the last
|
|
2040
|
+
// statement and has an expression.
|
|
2041
|
+
return interpretExpression(statement.expression as ts.Expression, ctx);
|
|
2042
|
+
}
|
|
2043
|
+
const declarations = (statement as ts.VariableStatement).declarationList.declarations;
|
|
2044
|
+
for (const decl of declarations) {
|
|
2045
|
+
const value = await interpretExpression(decl.initializer as ts.Expression, ctx);
|
|
2046
|
+
if (ts.isIdentifier(decl.name)) {
|
|
2047
|
+
bind(decl.name.text, value);
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
if (!isIndexableObject(value)) {
|
|
2051
|
+
throw cheapError(`destructured \`const\` source in "${briefNodeText(decl.name)}" is not an object`);
|
|
2052
|
+
}
|
|
2053
|
+
for (const el of (decl.name as ts.ObjectBindingPattern).elements) {
|
|
2054
|
+
bind(el.name.getText(), (value as Record<string, unknown>)[bindingElementPropKey(el) as string]);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
// Unreachable: rule 4 requires a trailing `return`.
|
|
2059
|
+
throw cheapError("composite factory body did not return");
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/**
|
|
2063
|
+
* Evaluate one expression of a factory body.
|
|
2064
|
+
*
|
|
2065
|
+
* Only the cases that can CONTAIN a construction or a composite call are
|
|
2066
|
+
* handled here; everything else delegates to {@link resolveDeclaratorValue},
|
|
2067
|
+
* which is the same `resolveLiveValue` -> `fold()` -> revive pipeline every
|
|
2068
|
+
* other value in a fold takes. That split is the point: the interpreter owns
|
|
2069
|
+
* as little evaluation semantics as it possibly can, so `fold()` stays the one
|
|
2070
|
+
* definition of what an expression means, and the operators/short-circuiting
|
|
2071
|
+
* duplicated below are duplicated because they must not evaluate a branch
|
|
2072
|
+
* `fold()` would not have evaluated.
|
|
2073
|
+
*/
|
|
2074
|
+
async function interpretExpression(node: ts.Expression, ctx: ResolveCtx): Promise<unknown> {
|
|
2075
|
+
if (
|
|
2076
|
+
ts.isParenthesizedExpression(node) ||
|
|
2077
|
+
ts.isAsExpression(node) ||
|
|
2078
|
+
ts.isSatisfiesExpression(node) ||
|
|
2079
|
+
ts.isNonNullExpression(node)
|
|
2080
|
+
) {
|
|
2081
|
+
return interpretExpression(node.expression, ctx);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
if (ts.isNewExpression(node)) return interpretNewExpression(node, ctx);
|
|
2085
|
+
|
|
2086
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
2087
|
+
const obj: Record<string, unknown> = {};
|
|
2088
|
+
for (const prop of node.properties) {
|
|
2089
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
2090
|
+
obj[propName(prop.name)] = await interpretExpression(prop.initializer, ctx);
|
|
2091
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
2092
|
+
obj[prop.name.text] = await interpretExpression(prop.name, ctx);
|
|
2093
|
+
} else {
|
|
2094
|
+
const src = await interpretExpression((prop as ts.SpreadAssignment).expression, ctx);
|
|
2095
|
+
if (src === null || typeof src !== "object") throw cheapError("spread source not an object");
|
|
2096
|
+
Object.assign(obj, src);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return obj;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
2103
|
+
const arr: unknown[] = [];
|
|
2104
|
+
for (const el of node.elements) {
|
|
2105
|
+
if (ts.isSpreadElement(el)) {
|
|
2106
|
+
const src = await interpretExpression(el.expression, ctx);
|
|
2107
|
+
if (!Array.isArray(src)) throw cheapError("spread source not an array");
|
|
2108
|
+
arr.push(...src);
|
|
2109
|
+
} else {
|
|
2110
|
+
arr.push(await interpretExpression(el, ctx));
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return arr;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (ts.isConditionalExpression(node)) {
|
|
2117
|
+
return (await interpretExpression(node.condition, ctx))
|
|
2118
|
+
? interpretExpression(node.whenTrue, ctx)
|
|
2119
|
+
: interpretExpression(node.whenFalse, ctx);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (ts.isBinaryExpression(node)) {
|
|
2123
|
+
const S = ts.SyntaxKind;
|
|
2124
|
+
const opKind = node.operatorToken.kind;
|
|
2125
|
+
if (opKind === S.AmpersandAmpersandToken) {
|
|
2126
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2127
|
+
return left ? interpretExpression(node.right, ctx) : left;
|
|
2128
|
+
}
|
|
2129
|
+
if (opKind === S.BarBarToken) {
|
|
2130
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2131
|
+
return left ? left : interpretExpression(node.right, ctx);
|
|
2132
|
+
}
|
|
2133
|
+
if (opKind === S.QuestionQuestionToken) {
|
|
2134
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2135
|
+
return left === null || left === undefined ? interpretExpression(node.right, ctx) : left;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
return (await resolveDeclaratorValue(node, ctx)).value;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/**
|
|
2143
|
+
* Construct a real resource from a `new Type(...)` anywhere inside a factory
|
|
2144
|
+
* body — a member, a nested property object, an array element.
|
|
2145
|
+
*
|
|
2146
|
+
* Deliberately NOT routed through {@link resolveResourceEntity}: that one
|
|
2147
|
+
* folds its arguments with {@link foldResource}, which rejects a nested `new`
|
|
2148
|
+
* as a value because a top-level fold has no way to construct one. Here there
|
|
2149
|
+
* IS a way — the defining module's imports are in hand — so each argument is
|
|
2150
|
+
* interpreted recursively and the constructor is called with exactly the
|
|
2151
|
+
* arguments the source wrote, which is what the run path does.
|
|
2152
|
+
*/
|
|
2153
|
+
async function interpretNewExpression(node: ts.NewExpression, ctx: ResolveCtx): Promise<unknown> {
|
|
2154
|
+
// Guaranteed an identifier by {@link checkFactoryExpression}; re-checked so a
|
|
2155
|
+
// future caller can't reach this with a dotted callee and get a silent miss.
|
|
2156
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
2157
|
+
throw cheapError(`\`new ${briefNodeText(node.expression)}(...)\` needs a plain imported constructor`);
|
|
2158
|
+
}
|
|
2159
|
+
const typeName = node.expression.text;
|
|
2160
|
+
const binding = ctx.imports.get(typeName);
|
|
2161
|
+
if (!binding) throw cheapError(`constructor "${typeName}" is not a resolvable import`);
|
|
2162
|
+
|
|
2163
|
+
// chant #1093 — same gate, same reason, as every other site that imports a
|
|
2164
|
+
// module in order to execute something from it.
|
|
2165
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, typeName, "constructor");
|
|
2166
|
+
if (refusal) throw cheapError(refusal);
|
|
2167
|
+
|
|
2168
|
+
const modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
2169
|
+
const mod = await importModuleMemoized(modulePath, ctx.importCache);
|
|
2170
|
+
const Ctor = mod[binding.imported];
|
|
2171
|
+
if (typeof Ctor !== "function") {
|
|
2172
|
+
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a constructor`);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
const ctorArgs: unknown[] = [];
|
|
2176
|
+
for (const arg of node.arguments ?? []) ctorArgs.push(await interpretExpression(arg, ctx));
|
|
2177
|
+
return new (Ctor as new (...ctorArguments: unknown[]) => unknown)(...ctorArgs);
|
|
1236
2178
|
}
|
|
1237
2179
|
|
|
1238
2180
|
/**
|
|
@@ -2085,6 +3027,8 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
2085
3027
|
resolvePathCache: session.resolvePathCache,
|
|
2086
3028
|
lexiconPackages: session.lexiconPackages,
|
|
2087
3029
|
sandbox: session.sandbox,
|
|
3030
|
+
session,
|
|
3031
|
+
interpretDepth: 0,
|
|
2088
3032
|
};
|
|
2089
3033
|
|
|
2090
3034
|
const entities: FoldedEntity[] = [];
|