@intentius/chant 0.20.0 → 0.22.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/build.d.ts +7 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +17 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +64 -3
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +12 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +89 -16
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +58 -3
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +73 -23
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +9 -0
- package/src/cli/commands/build.ts +9 -0
- package/src/cli/commands/check-lexicon-examples.ts +16 -1
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +35 -1
- package/src/cli/commands/check-lexicon-intrinsics.ts +38 -2
- package/src/cli/commands/check-lexicon.ts +18 -0
- package/src/codegen/docs-sections.test.ts +7 -1
- package/src/codegen/docs-sections.ts +1 -1
- package/src/codegen/docs-types.ts +2 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/entity-wire-codec.ts +9 -7
- package/src/discovery/fold-import.test.ts +900 -1
- package/src/discovery/fold-import.ts +441 -47
- package/src/discovery/index.ts +25 -1
- package/src/fold/fold.test.ts +277 -0
- package/src/fold/fold.ts +219 -58
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +27 -0
- package/src/fold/subset.test.ts +177 -0
- package/src/fold/subset.ts +139 -31
- package/src/lexicon-schema.test.ts +43 -0
- package/src/lexicon-schema.ts +5 -0
- package/src/lexicon.ts +74 -24
- package/src/runtime.ts +11 -2
- package/src/serializer-walker.ts +14 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync, statSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
-
import { dirname, basename, join, isAbsolute, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { dirname, basename, join, sep, isAbsolute, resolve as resolvePath } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
8
8
|
import { isCompositeInstance, type CompositeInstance } from "../composite";
|
|
9
9
|
import { isAttrRefLike } from "../utils";
|
|
10
|
+
import { isIntrinsic } from "../intrinsic";
|
|
10
11
|
import {
|
|
11
12
|
collectConsts,
|
|
12
13
|
foldResource,
|
|
@@ -16,8 +17,11 @@ import {
|
|
|
16
17
|
type FoldedResource,
|
|
17
18
|
type FoldedValue,
|
|
18
19
|
type FoldedIntrinsic,
|
|
20
|
+
type FoldedHelperCall,
|
|
19
21
|
type SymbolicValue,
|
|
20
22
|
} from "../fold/fold";
|
|
23
|
+
import { isChantOwnedSpecifier } from "../fold/foldable-helpers";
|
|
24
|
+
import { briefNodeText, callExpressionMessage } from "../fold/subset";
|
|
21
25
|
import { importModule } from "./import";
|
|
22
26
|
import type { IntrinsicDef } from "../lexicon";
|
|
23
27
|
import type { BuildParamValue } from "../build-params";
|
|
@@ -82,6 +86,20 @@ export type FoldFileResult =
|
|
|
82
86
|
* exact same object every referencing file sees.
|
|
83
87
|
*/
|
|
84
88
|
exportedValues: Map<string, unknown>;
|
|
89
|
+
/**
|
|
90
|
+
* chant #1044 — the OTHER project files whose exported OBJECTS this
|
|
91
|
+
* fold consumed (a cross-file `Declarable`, composite instance, or any
|
|
92
|
+
* other non-primitive reached through `buildExternals`/a re-export).
|
|
93
|
+
*
|
|
94
|
+
* Object identity is the thing that cannot survive one side of the
|
|
95
|
+
* build folding while the other runs, so `planFoldTaint` needs to know
|
|
96
|
+
* who consumed whose objects: if a file here is forced back to run, the
|
|
97
|
+
* instance THIS file already captured is not the instance discovery
|
|
98
|
+
* will collect, and serialization fails on an entity with no logical
|
|
99
|
+
* name. A primitive (string, number, boolean, null) is never recorded —
|
|
100
|
+
* it has no identity to disagree about.
|
|
101
|
+
*/
|
|
102
|
+
liveSources: ReadonlySet<string>;
|
|
85
103
|
}
|
|
86
104
|
| { ok: false; reason: string };
|
|
87
105
|
|
|
@@ -155,14 +173,58 @@ export interface FoldSession {
|
|
|
155
173
|
* doesn't use build-time parameters pays nothing extra here.
|
|
156
174
|
*/
|
|
157
175
|
readonly buildParams?: Readonly<Record<string, BuildParamValue>>;
|
|
176
|
+
/**
|
|
177
|
+
* chant #1063 — the exact package specifiers of the lexicons LOADED for
|
|
178
|
+
* this build (`@intentius/chant-lexicon-aws`, …), derived from the lexicon
|
|
179
|
+
* names the build already resolved (`resolveProjectLexicons` ->
|
|
180
|
+
* `loadPlugins`, see ../cli/plugins.ts). This is the entire allowlist
|
|
181
|
+
* {@link buildExternals} will follow a bare import specifier into — see
|
|
182
|
+
* {@link activeLexiconPackage} for why the set is matched by TEXT and
|
|
183
|
+
* built from names the build already knows, rather than by resolving
|
|
184
|
+
* specifiers to find out what they are.
|
|
185
|
+
*
|
|
186
|
+
* Empty when the caller supplied no lexicon list, which disables
|
|
187
|
+
* lexicon-package resolution entirely rather than falling back to
|
|
188
|
+
* something more permissive: "an active lexicon of this build" is the
|
|
189
|
+
* boundary, and a build that can't say what its lexicons are hasn't
|
|
190
|
+
* established one.
|
|
191
|
+
*/
|
|
192
|
+
readonly lexiconPackages: ReadonlySet<string>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* chant #1063 — the package specifier a lexicon NAME (`"aws"`, `"gitlab"`)
|
|
197
|
+
* is installed under. The one naming convention the whole CLI already
|
|
198
|
+
* depends on: `loadPlugin(name)` imports exactly this
|
|
199
|
+
* (../cli/plugins.ts), `detectLexicons` scans source for exactly this
|
|
200
|
+
* (../detectLexicon.ts), and `chant init` writes exactly this into
|
|
201
|
+
* package.json.
|
|
202
|
+
*/
|
|
203
|
+
export function lexiconPackageName(lexiconName: string): string {
|
|
204
|
+
return `@intentius/chant-lexicon-${lexiconName}`;
|
|
158
205
|
}
|
|
159
206
|
|
|
160
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Create a fresh, empty {@link FoldSession}.
|
|
209
|
+
*
|
|
210
|
+
* @param lexicons - chant #1063: the lexicon NAMES active for this build
|
|
211
|
+
* (`["aws", "k8s"]`). Converted to package specifiers via
|
|
212
|
+
* {@link lexiconPackageName}; see {@link FoldSession.lexiconPackages}.
|
|
213
|
+
*/
|
|
161
214
|
export function createFoldSession(
|
|
162
215
|
intrinsics: readonly IntrinsicDef[] = [],
|
|
163
216
|
buildParams?: Readonly<Record<string, BuildParamValue>>,
|
|
217
|
+
lexicons: readonly string[] = [],
|
|
164
218
|
): FoldSession {
|
|
165
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
intrinsics,
|
|
221
|
+
cache: new Map(),
|
|
222
|
+
stack: [],
|
|
223
|
+
importCache: new Map(),
|
|
224
|
+
resolvePathCache: new Map(),
|
|
225
|
+
buildParams,
|
|
226
|
+
lexiconPackages: new Set(lexicons.map(lexiconPackageName)),
|
|
227
|
+
};
|
|
166
228
|
}
|
|
167
229
|
|
|
168
230
|
/**
|
|
@@ -789,6 +851,100 @@ function paramsModulePath(): string | null {
|
|
|
789
851
|
return paramsModulePathMemo;
|
|
790
852
|
}
|
|
791
853
|
|
|
854
|
+
/**
|
|
855
|
+
* chant #1063 — is `specifier` a bare import of a package that is an ACTIVE
|
|
856
|
+
* LEXICON of this build? Returns the specifier itself when so, `undefined`
|
|
857
|
+
* otherwise.
|
|
858
|
+
*
|
|
859
|
+
* Three deliberate restrictions, each of which is the point rather than an
|
|
860
|
+
* omission:
|
|
861
|
+
*
|
|
862
|
+
* - **Text only, no resolution.** The answer is a `Set.has` on a set built
|
|
863
|
+
* from the lexicon names the build ALREADY resolved before discovery ran
|
|
864
|
+
* (`resolveProjectLexicons` -> `loadPlugins`). Nothing is probed, read, or
|
|
865
|
+
* resolved to decide whether a specifier is in scope — so an import of
|
|
866
|
+
* some unrelated package costs a string comparison and is then left alone,
|
|
867
|
+
* never resolved "just to find out what it is". That matters here more
|
|
868
|
+
* than anywhere: `resolveModulePath`'s bare branch can fall through to
|
|
869
|
+
* `createRequire(fromFile).resolve(specifier)`, measured at up to ~361s
|
|
870
|
+
* for the first resolution of a genuinely new bare specifier in a process
|
|
871
|
+
* (see {@link bareSpecifierPathCache}/{@link fastResolveBareSpecifier} —
|
|
872
|
+
* this exact class of cost regressed twice during chant#1020).
|
|
873
|
+
*
|
|
874
|
+
* - **Exact package specifier, no subpaths.** `@intentius/chant-lexicon-aws`
|
|
875
|
+
* matches; `@intentius/chant-lexicon-aws/actions` does not. Every lexicon
|
|
876
|
+
* re-exports its whole public surface from its barrel and every example
|
|
877
|
+
* imports it that way, so subpaths buy nothing — and they would cost
|
|
878
|
+
* something real: {@link fastResolveBareSpecifier} only recognizes a
|
|
879
|
+
* package ROOT (it looks for `<node_modules>/<specifier>/package.json`),
|
|
880
|
+
* so a subpath specifier falls through to exactly the slow
|
|
881
|
+
* `createRequire().resolve()` path this restriction exists to avoid.
|
|
882
|
+
*
|
|
883
|
+
* - **Lexicons of THIS build only.** Not "any `@intentius/chant-lexicon-*`
|
|
884
|
+
* package on disk", and emphatically not "any bare specifier". A lexicon
|
|
885
|
+
* the build did not load is as out of scope as `node:fs`.
|
|
886
|
+
*/
|
|
887
|
+
function activeLexiconPackage(specifier: string, lexiconPackages: ReadonlySet<string>): string | undefined {
|
|
888
|
+
return lexiconPackages.has(specifier) ? specifier : undefined;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* chant #1063 — resolve one named import binding against an active lexicon
|
|
893
|
+
* package's REAL exports, for {@link buildExternals}.
|
|
894
|
+
*
|
|
895
|
+
* The lexicon module is imported (through the session-wide
|
|
896
|
+
* {@link FoldSession.importCache}, so at most one real `import()` per package
|
|
897
|
+
* per build) and the requested export read straight off it. That is the same
|
|
898
|
+
* module object the run path gets — `importModule` is a plain dynamic
|
|
899
|
+
* `import()` with no cache-busting — so what fold captures here is not a
|
|
900
|
+
* reconstruction of the lexicon's data but the identical value, down to
|
|
901
|
+
* object identity for `Azure.ResourceGroupLocation`-style singletons. It is
|
|
902
|
+
* also the same two-step (resolve path, then import) that
|
|
903
|
+
* `resolveImportedExport` has always used to reach a lexicon's constructors
|
|
904
|
+
* and intrinsic functions; the only thing new is that a plain DATA export is
|
|
905
|
+
* now reachable too.
|
|
906
|
+
*
|
|
907
|
+
* Callable exports are deliberately excluded. A lexicon's functions — its
|
|
908
|
+
* resource classes, composite factories, intrinsic implementations — already
|
|
909
|
+
* have dedicated resolution paths that know how to INVOKE them
|
|
910
|
+
* (`resolveResourceEntity`, `resolveCallExpression`, `reviveFoldedValue`), and
|
|
911
|
+
* binding them as plain identifier values here would widen what folds in ways
|
|
912
|
+
* this issue neither needs nor measured: none of the references #1063 exists
|
|
913
|
+
* to unblock (`Azure`, `GCP`, `S3Actions`, gitlab's `CI`) is a function.
|
|
914
|
+
*
|
|
915
|
+
* Returns `undefined` — never throws — for a package that isn't an active
|
|
916
|
+
* lexicon, an export the package doesn't have, a callable export, or an
|
|
917
|
+
* import that fails. The binding is then simply absent from `externals`,
|
|
918
|
+
* exactly as before, and `fold()`'s ordinary "unresolved identifier" failure
|
|
919
|
+
* still fires if the name is actually referenced.
|
|
920
|
+
*/
|
|
921
|
+
async function resolveActiveLexiconExport(
|
|
922
|
+
binding: ImportBinding,
|
|
923
|
+
fromFile: string,
|
|
924
|
+
session: FoldSession,
|
|
925
|
+
): Promise<{ value: unknown } | undefined> {
|
|
926
|
+
if (!activeLexiconPackage(binding.specifier, session.lexiconPackages)) return undefined;
|
|
927
|
+
|
|
928
|
+
let modulePath: string;
|
|
929
|
+
try {
|
|
930
|
+
modulePath = resolveModulePathMemoized(binding.specifier, fromFile, session.resolvePathCache);
|
|
931
|
+
} catch {
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
let mod: Record<string, unknown>;
|
|
936
|
+
try {
|
|
937
|
+
mod = await importModuleMemoized(modulePath, session.importCache);
|
|
938
|
+
} catch {
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (!(binding.imported in mod)) return undefined;
|
|
943
|
+
const value = mod[binding.imported];
|
|
944
|
+
if (typeof value === "function") return undefined;
|
|
945
|
+
return { value };
|
|
946
|
+
}
|
|
947
|
+
|
|
792
948
|
// ─────────────────────────────────────────────────────────────────────────
|
|
793
949
|
// Resolution: given the scan + import map, compute the REAL runtime value
|
|
794
950
|
// (Declarable | CompositeInstance) each foldable export would have had if
|
|
@@ -946,13 +1102,18 @@ async function resolveLiveValue(node: ts.Expression, ctx: ResolveCtx): Promise<L
|
|
|
946
1102
|
* `isDeclarable`/`isCompositeInstance` value for a top-level export).
|
|
947
1103
|
*/
|
|
948
1104
|
async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx): Promise<unknown> {
|
|
1105
|
+
// chant #1054 — reuses ../fold/subset's `callExpressionMessage` (the SAME
|
|
1106
|
+
// builder `fold()` throws with for a call used as a prop value) rather
|
|
1107
|
+
// than a hand-written "call expression as a value" copy: the two used to
|
|
1108
|
+
// say different things for the identical rejection, which silently broke
|
|
1109
|
+
// any tooling grouping fallback reasons by text.
|
|
949
1110
|
if (!ts.isIdentifier(node.expression)) {
|
|
950
|
-
throw cheapError(
|
|
1111
|
+
throw cheapError(callExpressionMessage(node));
|
|
951
1112
|
}
|
|
952
1113
|
const calleeName = node.expression.text;
|
|
953
1114
|
const binding = ctx.imports.get(calleeName);
|
|
954
1115
|
if (!binding) {
|
|
955
|
-
throw cheapError(
|
|
1116
|
+
throw cheapError(callExpressionMessage(node));
|
|
956
1117
|
}
|
|
957
1118
|
|
|
958
1119
|
let modulePath: string;
|
|
@@ -1114,13 +1275,23 @@ async function resolveSymbolicValue(text: string, ctx: ResolveCtx): Promise<unkn
|
|
|
1114
1275
|
}
|
|
1115
1276
|
|
|
1116
1277
|
/**
|
|
1117
|
-
* Revive a folded value tree: replace any
|
|
1118
|
-
* envelope with the real value it
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1278
|
+
* Revive a folded value tree: replace any
|
|
1279
|
+
* `{__intrinsic}`/`{__helper}`/`{__symbol}` envelope with the real value it
|
|
1280
|
+
* represents.
|
|
1281
|
+
*
|
|
1282
|
+
* `requireLiveRefs` tracks whether the CURRENT node is (transitively) an
|
|
1283
|
+
* argument being handed to a real function that will inspect it — a
|
|
1284
|
+
* `{__intrinsic}`'s own interpolated `values`, or a `{__helper}` call's
|
|
1285
|
+
* arguments (chant #1082). In that position a symbolic `{__attrRef}` envelope
|
|
1286
|
+
* is rejected rather than passed along: the receiving implementation needs a
|
|
1287
|
+
* genuine `AttrRef` instance (`instanceof` checks, `WeakRef` derefs — see
|
|
1288
|
+
* `SubIntrinsic`, and `LexiconOutput`'s constructor in ../lexicon-output.ts),
|
|
1289
|
+
* and handing it a look-alike plain object produces output that is wrong
|
|
1290
|
+
* rather than absent. Everywhere else the envelope is left untouched, because
|
|
1291
|
+
* the serializer's generic walker already understands it — see the module-doc
|
|
1292
|
+
* note above.
|
|
1122
1293
|
*/
|
|
1123
|
-
async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx,
|
|
1294
|
+
async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, requireLiveRefs: boolean): Promise<unknown> {
|
|
1124
1295
|
if (value === null || typeof value !== "object") return value;
|
|
1125
1296
|
|
|
1126
1297
|
// chant #1020 — a REAL, already-constructed live object reached via
|
|
@@ -1135,13 +1306,23 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, insideIntr
|
|
|
1135
1306
|
// destroying the very identity #1020 exists to preserve. Passed through
|
|
1136
1307
|
// completely unchanged, exactly like `resolveCallExpression`'s own
|
|
1137
1308
|
// `live.value` passthrough for a composite-call argument.
|
|
1138
|
-
|
|
1309
|
+
// chant #1063 adds the third kind of real object cross-file resolution can
|
|
1310
|
+
// now put here: a live `Intrinsic` instance read off an active lexicon
|
|
1311
|
+
// package (`Azure.ResourceGroupLocation`, `GCP.ProjectId` — see
|
|
1312
|
+
// `resolveActiveLexiconExport`). Same hazard, same fix: the generic walk
|
|
1313
|
+
// below would rebuild it as a plain `{}` copy, dropping the prototype that
|
|
1314
|
+
// carries `toJSON()` and so serializing `{}` where the run path emits
|
|
1315
|
+
// `[resourceGroup().location]`. `isIntrinsic` keys off
|
|
1316
|
+
// `Symbol.for("chant.intrinsic")` (../intrinsic.ts), a GLOBAL symbol, so it
|
|
1317
|
+
// holds across separately-loaded copies of chant-core the way a bare
|
|
1318
|
+
// `instanceof` would not.
|
|
1319
|
+
if (isAttrRefLike(value) || isDeclarable(value) || isCompositeInstance(value) || isIntrinsic(value)) {
|
|
1139
1320
|
return value;
|
|
1140
1321
|
}
|
|
1141
1322
|
|
|
1142
1323
|
if (Array.isArray(value)) {
|
|
1143
1324
|
const revived: unknown[] = [];
|
|
1144
|
-
for (const el of value) revived.push(await reviveFoldedValue(el, ctx,
|
|
1325
|
+
for (const el of value) revived.push(await reviveFoldedValue(el, ctx, requireLiveRefs));
|
|
1145
1326
|
return revived;
|
|
1146
1327
|
}
|
|
1147
1328
|
|
|
@@ -1154,17 +1335,33 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, insideIntr
|
|
|
1154
1335
|
const intrinsic = value as FoldedIntrinsic;
|
|
1155
1336
|
const Fn = await resolveImportedExport(intrinsic.__intrinsic, ctx);
|
|
1156
1337
|
if (typeof Fn !== "function") {
|
|
1157
|
-
throw cheapError(`intrinsic
|
|
1338
|
+
throw cheapError(`intrinsic "${intrinsic.__intrinsic}" did not resolve to a function`);
|
|
1158
1339
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1340
|
+
// Two authored forms, one envelope family (../fold/fold.ts's
|
|
1341
|
+
// `FoldedIntrinsic`): the tagged template replays as
|
|
1342
|
+
// `Name(strings, ...values)`, the plain call (chant #1044) as
|
|
1343
|
+
// `Name(...args)`. Both hand their interior to the REAL function the
|
|
1344
|
+
// file itself imported, with `requireLiveRefs` — an intrinsic inspects
|
|
1345
|
+
// what it is given (`SubIntrinsic`'s `instanceof` checks, `Ref`'s
|
|
1346
|
+
// `getLogicalName`), so a look-alike `{__attrRef}` envelope must be
|
|
1347
|
+
// rejected here rather than silently serialized as something else.
|
|
1348
|
+
const revived: unknown[] = [];
|
|
1349
|
+
if ("args" in intrinsic) {
|
|
1350
|
+
for (const a of intrinsic.args) revived.push(await reviveFoldedValue(a, ctx, true));
|
|
1351
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(...revived);
|
|
1352
|
+
}
|
|
1353
|
+
for (const v of intrinsic.values) revived.push(await reviveFoldedValue(v, ctx, true));
|
|
1354
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(intrinsic.strings, ...revived);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if ("__helper" in value) {
|
|
1358
|
+
return reviveHelperCall(value as FoldedHelperCall, ctx);
|
|
1162
1359
|
}
|
|
1163
1360
|
|
|
1164
1361
|
if ("__attrRef" in value) {
|
|
1165
|
-
if (
|
|
1362
|
+
if (requireLiveRefs) {
|
|
1166
1363
|
throw cheapError(
|
|
1167
|
-
"a same-file resource reference
|
|
1364
|
+
"a same-file resource reference passed to a folded intrinsic or authoring helper is not foldable yet",
|
|
1168
1365
|
);
|
|
1169
1366
|
}
|
|
1170
1367
|
return value;
|
|
@@ -1178,11 +1375,93 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, insideIntr
|
|
|
1178
1375
|
|
|
1179
1376
|
const revived: Record<string, unknown> = {};
|
|
1180
1377
|
for (const [key, v] of Object.entries(value)) {
|
|
1181
|
-
revived[key] = await reviveFoldedValue(v as FoldedValue, ctx,
|
|
1378
|
+
revived[key] = await reviveFoldedValue(v as FoldedValue, ctx, requireLiveRefs);
|
|
1182
1379
|
}
|
|
1183
1380
|
return revived;
|
|
1184
1381
|
}
|
|
1185
1382
|
|
|
1383
|
+
/**
|
|
1384
|
+
* chant #1082 — the provenance half of the registered-authoring-helper check
|
|
1385
|
+
* (the shape/name half is `fold()`'s, see ../fold/foldable-helpers.ts's module
|
|
1386
|
+
* doc). Being in the allowlist is not permission to invoke whatever the name
|
|
1387
|
+
* happens to be bound to: the name must be bound by THIS FILE'S OWN `import`,
|
|
1388
|
+
* and that import must actually come from chant. Only then is the real
|
|
1389
|
+
* function called, with the folded arguments — the same function the run path
|
|
1390
|
+
* would have called, from the same module the source itself named, so fold
|
|
1391
|
+
* cannot diverge from run by construction.
|
|
1392
|
+
*
|
|
1393
|
+
* Anything short of that throws, which falls the whole file back to run: a
|
|
1394
|
+
* local `function phase(...)`, a `phase` imported from the project's own
|
|
1395
|
+
* helpers, a chant-owned import that turns out not to be a function.
|
|
1396
|
+
*/
|
|
1397
|
+
async function reviveHelperCall(call: FoldedHelperCall, ctx: ResolveCtx): Promise<unknown> {
|
|
1398
|
+
const name = call.__helper;
|
|
1399
|
+
const binding = ctx.imports.get(name);
|
|
1400
|
+
if (!binding) {
|
|
1401
|
+
throw cheapError(`authoring helper "${name}(...)" is not a resolvable import`);
|
|
1402
|
+
}
|
|
1403
|
+
if (!isChantOwnedHelperBinding(binding, ctx)) {
|
|
1404
|
+
throw cheapError(
|
|
1405
|
+
`"${name}" is imported from "${binding.specifier}", which is not chant's own — ` +
|
|
1406
|
+
`only chant's registered authoring helpers fold as calls`,
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const Fn = await resolveImportedExport(name, ctx);
|
|
1411
|
+
if (typeof Fn !== "function") {
|
|
1412
|
+
throw cheapError(`authoring helper "${name}" did not resolve to a function`);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// `requireLiveRefs` — a helper receives its arguments as real values and may
|
|
1416
|
+
// inspect them (`output()` derefs the ref's `WeakRef` parent), so a symbolic
|
|
1417
|
+
// `{__attrRef}` envelope is rejected here rather than silently wrapped into
|
|
1418
|
+
// a wrong result. A ref that resolved cross-file to a genuine live `AttrRef`
|
|
1419
|
+
// passes straight through (see `reviveFoldedValue`'s early return).
|
|
1420
|
+
const args: unknown[] = [];
|
|
1421
|
+
for (const arg of call.args) args.push(await reviveFoldedValue(arg, ctx, true));
|
|
1422
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(...args);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* True when `binding` names a helper import chant itself owns: a published
|
|
1427
|
+
* chant package specifier ({@link isChantOwnedSpecifier}), or — for in-repo
|
|
1428
|
+
* and test callers, which import chant-core by relative/absolute path the way
|
|
1429
|
+
* this module's own fixtures do — a specifier that resolves to a file inside
|
|
1430
|
+
* chant-core's own tree.
|
|
1431
|
+
*
|
|
1432
|
+
* The path arm resolves only for a relative/absolute specifier, never a bare
|
|
1433
|
+
* one: resolving an arbitrary bare specifier here would reintroduce the
|
|
1434
|
+
* pathological cold-resolution cost chant#1020 measured (see
|
|
1435
|
+
* {@link fastResolveBareSpecifier}), and a bare specifier chant publishes is
|
|
1436
|
+
* already covered by the text arm.
|
|
1437
|
+
*/
|
|
1438
|
+
function isChantOwnedHelperBinding(binding: ImportBinding, ctx: ResolveCtx): boolean {
|
|
1439
|
+
if (isChantOwnedSpecifier(binding.specifier)) return true;
|
|
1440
|
+
if (!isProjectFileSpecifier(binding.specifier)) return false;
|
|
1441
|
+
let targetPath: string;
|
|
1442
|
+
try {
|
|
1443
|
+
targetPath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
1444
|
+
} catch {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
const root = chantCoreRoot();
|
|
1448
|
+
return targetPath === root || targetPath.startsWith(root + sep);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* chant-core's own module root — `packages/core/src` in this repo,
|
|
1453
|
+
* `<pkg>/dist` in a published install — derived from THIS module's location.
|
|
1454
|
+
* Pure string arithmetic on `import.meta.url`, no filesystem access at all, so
|
|
1455
|
+
* it is safe in the #1045 sandbox child (which locks reads to an allowlist);
|
|
1456
|
+
* lazy and memoized purely to avoid doing it per call.
|
|
1457
|
+
*/
|
|
1458
|
+
let chantCoreRootMemo: string | undefined;
|
|
1459
|
+
function chantCoreRoot(): string {
|
|
1460
|
+
// .../<root>/discovery/fold-import.ts -> .../<root>
|
|
1461
|
+
chantCoreRootMemo ??= dirname(dirname(fileURLToPath(import.meta.url)));
|
|
1462
|
+
return chantCoreRootMemo;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1186
1465
|
/** Revive every value in a folded props/attributes object (see {@link reviveFoldedValue}). */
|
|
1187
1466
|
async function reviveFoldedProps(
|
|
1188
1467
|
props: { [key: string]: FoldedValue },
|
|
@@ -1220,11 +1499,27 @@ async function resolveResourceEntity(
|
|
|
1220
1499
|
// real runtime values before constructing the entity. A no-op walk when
|
|
1221
1500
|
// this file used no registered intrinsics (the overwhelming majority of
|
|
1222
1501
|
// cases today).
|
|
1223
|
-
|
|
1224
|
-
|
|
1502
|
+
//
|
|
1503
|
+
// chant #1082 — when `spec.args` is present the constructor's argument list
|
|
1504
|
+
// isn't the classic `(props)`/`(props, attributes)` shape (AWS's `Parameter`
|
|
1505
|
+
// is `(type, props)`), so revive the whole list and spread it below instead
|
|
1506
|
+
// of reviving `spec.props`, which in that case is only a view onto one of
|
|
1507
|
+
// its entries and would be double-counted.
|
|
1508
|
+
let ctorArgs: unknown[];
|
|
1225
1509
|
try {
|
|
1226
|
-
|
|
1227
|
-
|
|
1510
|
+
if (spec.args) {
|
|
1511
|
+
ctorArgs = [];
|
|
1512
|
+
for (const arg of spec.args) ctorArgs.push(await reviveFoldedValue(arg, ctx, false));
|
|
1513
|
+
} else {
|
|
1514
|
+
const props = await reviveFoldedProps(spec.props, ctx);
|
|
1515
|
+
// The runtime constructor's optional second argument (`attributes` —
|
|
1516
|
+
// CFN's DependsOn/Condition/DeletionPolicy/…, see createResource in
|
|
1517
|
+
// ../runtime.ts) is only present in `spec` when the source actually
|
|
1518
|
+
// passed one (see foldResource in ../fold/fold.ts). Passing `undefined`
|
|
1519
|
+
// when it's absent matches the run path's own default
|
|
1520
|
+
// (`attributes ?? {}` inside the constructor).
|
|
1521
|
+
ctorArgs = [props, spec.attributes ? await reviveFoldedProps(spec.attributes, ctx) : undefined];
|
|
1522
|
+
}
|
|
1228
1523
|
} catch (err) {
|
|
1229
1524
|
return {
|
|
1230
1525
|
ok: false,
|
|
@@ -1264,17 +1559,10 @@ async function resolveResourceEntity(
|
|
|
1264
1559
|
return { ok: false, reason: `"${binding.imported}" from "${binding.specifier}" is not a constructor` };
|
|
1265
1560
|
}
|
|
1266
1561
|
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
// absent matches the run path's own default (`attributes ?? {}` inside the
|
|
1272
|
-
// constructor).
|
|
1273
|
-
const ResourceCtor = Ctor as new (
|
|
1274
|
-
props: Record<string, unknown>,
|
|
1275
|
-
attributes?: Record<string, unknown>,
|
|
1276
|
-
) => Declarable;
|
|
1277
|
-
const entity = new ResourceCtor(props, attributes);
|
|
1562
|
+
// Constructed with exactly the arguments the source wrote (see the revival
|
|
1563
|
+
// block above for how `ctorArgs` was built for each of the two shapes).
|
|
1564
|
+
const ResourceCtor = Ctor as new (...ctorArguments: unknown[]) => Declarable;
|
|
1565
|
+
const entity = new ResourceCtor(...ctorArgs);
|
|
1278
1566
|
return { ok: true, entity };
|
|
1279
1567
|
}
|
|
1280
1568
|
|
|
@@ -1310,6 +1598,18 @@ function describeFoldFailure(err: unknown, ctx: ResolveCtx): string {
|
|
|
1310
1598
|
return err.message;
|
|
1311
1599
|
}
|
|
1312
1600
|
|
|
1601
|
+
/**
|
|
1602
|
+
* chant #1054 — a short, single-line label for a destructured export's
|
|
1603
|
+
* source expression, for a fold fallback reason: the callee plus `(...)`
|
|
1604
|
+
* for the common composite-call source (`GkeCluster(...)`), or a brief,
|
|
1605
|
+
* bounded rendering of whatever else it is otherwise. Never the source's own
|
|
1606
|
+
* `getText()` — for a real composite call that's the entire multi-line
|
|
1607
|
+
* argument list.
|
|
1608
|
+
*/
|
|
1609
|
+
function describeDestructureSource(node: ts.Expression): string {
|
|
1610
|
+
return ts.isCallExpression(node) ? `${briefNodeText(node.expression)}(...)` : briefNodeText(node);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1313
1613
|
/** Build a located `FoldError`'s formatted "line:col - message" string anchored at `node` — for a cross-file failure detected here in fold-import.ts (an import cycle, a name genuinely absent from the target module's exports) rather than inside `fold()` itself. */
|
|
1314
1614
|
function locatedMessage(node: ts.Node, message: string): string {
|
|
1315
1615
|
const { line, column } = locate(node);
|
|
@@ -1394,9 +1694,11 @@ async function buildExternals(
|
|
|
1394
1694
|
imports: Map<string, ImportBinding>,
|
|
1395
1695
|
namespaceImports: Map<string, NamespaceImportBinding>,
|
|
1396
1696
|
session: FoldSession,
|
|
1397
|
-
): Promise<{ externals: Map<string, unknown>; failures: Map<string, string> }> {
|
|
1697
|
+
): Promise<{ externals: Map<string, unknown>; failures: Map<string, string>; liveSources: Set<string> }> {
|
|
1398
1698
|
const externals = new Map<string, unknown>();
|
|
1399
1699
|
const failures = new Map<string, string>();
|
|
1700
|
+
// chant #1044 — see `FoldFileResult.liveSources`.
|
|
1701
|
+
const liveSources = new Set<string>();
|
|
1400
1702
|
|
|
1401
1703
|
for (const [localName, binding] of imports) {
|
|
1402
1704
|
// chant #1064 — a named `params` import that resolves to chant-core's own
|
|
@@ -1450,10 +1752,36 @@ async function buildExternals(
|
|
|
1450
1752
|
}
|
|
1451
1753
|
|
|
1452
1754
|
if (!isProjectFileSpecifier(binding.specifier)) {
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1755
|
+
// chant #1063 — a bare specifier naming one of THIS BUILD's active
|
|
1756
|
+
// lexicon packages resolves to that package's real export, so a plain
|
|
1757
|
+
// data export a lexicon publishes (`Azure`/`GCP`'s pseudo-parameter
|
|
1758
|
+
// namespaces, AWS's `S3Actions`, gitlab's `CI`) is an ordinary
|
|
1759
|
+
// identifier value here rather than fold's most common remaining
|
|
1760
|
+
// "unresolved identifier" failure. See
|
|
1761
|
+
// {@link resolveActiveLexiconExport} for the allowlist, the no-cold-
|
|
1762
|
+
// resolution rule, and why callable exports stay out.
|
|
1763
|
+
//
|
|
1764
|
+
// No `liveSources` edge is recorded for what comes back, unlike the
|
|
1765
|
+
// project-file case just below. `liveSources` (chant #1044) exists so
|
|
1766
|
+
// that a folded file which captured ANOTHER FILE's objects is
|
|
1767
|
+
// invalidated when that file is forced back to run — a fold/run
|
|
1768
|
+
// disagreement about identity. A lexicon package has no such duality:
|
|
1769
|
+
// it is not a discovered source file, `planFoldTaint` never considers
|
|
1770
|
+
// it (it filters to the discovered `files` set), it never falls back to
|
|
1771
|
+
// run, and both paths reach it through the identical un-cache-busted
|
|
1772
|
+
// `import()` of the identical resolved path — so the object fold
|
|
1773
|
+
// captures IS the object the run path holds. There is nothing for the
|
|
1774
|
+
// two sides to disagree about, hence nothing to taint.
|
|
1775
|
+
const lexiconExport = await resolveActiveLexiconExport(binding, file, session);
|
|
1776
|
+
if (lexiconExport) {
|
|
1777
|
+
externals.set(localName, lexiconExport.value);
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
// Every other bare specifier (a non-lexicon vendor package, a lexicon
|
|
1781
|
+
// this build didn't load) is left alone here, exactly as before #1064:
|
|
1782
|
+
// it resolves lazily, through the pre-existing `importModule`
|
|
1783
|
+
// mechanism, only once a constructor/composite-factory/intrinsic tag
|
|
1784
|
+
// actually consumes it.
|
|
1457
1785
|
continue;
|
|
1458
1786
|
}
|
|
1459
1787
|
let targetPath: string;
|
|
@@ -1468,7 +1796,9 @@ async function buildExternals(
|
|
|
1468
1796
|
continue;
|
|
1469
1797
|
}
|
|
1470
1798
|
if (result.exportedValues.has(binding.imported)) {
|
|
1471
|
-
|
|
1799
|
+
const value = result.exportedValues.get(binding.imported);
|
|
1800
|
+
externals.set(localName, value);
|
|
1801
|
+
if (hasObjectIdentity(value)) liveSources.add(targetPath);
|
|
1472
1802
|
} else {
|
|
1473
1803
|
failures.set(
|
|
1474
1804
|
localName,
|
|
@@ -1494,9 +1824,28 @@ async function buildExternals(
|
|
|
1494
1824
|
// access on it (`ns.someExport`) is then just an ordinary bracket index,
|
|
1495
1825
|
// exactly like on a real composite instance (see `isIndexableObject`).
|
|
1496
1826
|
externals.set(localName, Object.fromEntries(result.exportedValues));
|
|
1827
|
+
for (const value of result.exportedValues.values()) {
|
|
1828
|
+
if (hasObjectIdentity(value)) {
|
|
1829
|
+
liveSources.add(targetPath);
|
|
1830
|
+
break;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1497
1833
|
}
|
|
1498
1834
|
|
|
1499
|
-
return { externals, failures };
|
|
1835
|
+
return { externals, failures, liveSources };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* True when `value` is something whose IDENTITY matters across the
|
|
1840
|
+
* fold/run boundary — any object or function, as opposed to a primitive
|
|
1841
|
+
* (chant #1044). Deliberately coarse: an object that merely *contains* a
|
|
1842
|
+
* `Declarable` is as identity-bearing as the Declarable itself, and cheaply
|
|
1843
|
+
* treating every object as such avoids a deep walk whose only payoff would
|
|
1844
|
+
* be keeping a handful of extra files folded inside an entry that already
|
|
1845
|
+
* falls back. See {@link FoldFileResult.liveSources}.
|
|
1846
|
+
*/
|
|
1847
|
+
function hasObjectIdentity(value: unknown): boolean {
|
|
1848
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
1500
1849
|
}
|
|
1501
1850
|
|
|
1502
1851
|
/**
|
|
@@ -1540,7 +1889,7 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
1540
1889
|
if (scan.declarators.length === 0) return { ok: false, reason: "no foldable resource exports" };
|
|
1541
1890
|
|
|
1542
1891
|
const collected = collectImports(sourceFile);
|
|
1543
|
-
const { externals, failures } = await buildExternals(file, collected.named, collected.namespaces, session);
|
|
1892
|
+
const { externals, failures, liveSources } = await buildExternals(file, collected.named, collected.namespaces, session);
|
|
1544
1893
|
|
|
1545
1894
|
const ctx: ResolveCtx = {
|
|
1546
1895
|
file,
|
|
@@ -1580,18 +1929,26 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
1580
1929
|
|
|
1581
1930
|
if (decl.kind === "destructure") {
|
|
1582
1931
|
let value: unknown;
|
|
1932
|
+
// chant #1054 — identify the destructured export by its BINDING
|
|
1933
|
+
// NAMES and the source's callee (`"cluster, nodePool" (destructured
|
|
1934
|
+
// from GkeCluster(...))`), never `decl.node.getText()`: for a real
|
|
1935
|
+
// composite call that's the entire multi-line source, which buries
|
|
1936
|
+
// the actual error after it and breaks any line-oriented consumer of
|
|
1937
|
+
// `[fold:run]` output.
|
|
1938
|
+
const boundNames = decl.elements.map((el) => el.bindingName).join(", ");
|
|
1939
|
+
const source = describeDestructureSource(decl.node);
|
|
1583
1940
|
try {
|
|
1584
1941
|
value = (await resolveDeclaratorValue(decl.node, ctx)).value;
|
|
1585
1942
|
} catch (err) {
|
|
1586
1943
|
return {
|
|
1587
1944
|
ok: false,
|
|
1588
|
-
reason: `destructured
|
|
1945
|
+
reason: `"${boundNames}" (destructured from ${source}) is not foldable: ${describeFoldFailure(err, ctx)}`,
|
|
1589
1946
|
};
|
|
1590
1947
|
}
|
|
1591
1948
|
if (!isIndexableObject(value)) {
|
|
1592
1949
|
return {
|
|
1593
1950
|
ok: false,
|
|
1594
|
-
reason: `destructured
|
|
1951
|
+
reason: `"${boundNames}" (destructured from ${source}) is not foldable: not a composite call or object`,
|
|
1595
1952
|
};
|
|
1596
1953
|
}
|
|
1597
1954
|
for (const { propKey, bindingName } of decl.elements) {
|
|
@@ -1644,11 +2001,16 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
1644
2001
|
reason: locatedMessage(decl.specifierNode, `"${imported}" is not exported by "${decl.specifier}"`),
|
|
1645
2002
|
};
|
|
1646
2003
|
}
|
|
1647
|
-
|
|
2004
|
+
const value = result.exportedValues.get(imported);
|
|
2005
|
+
// chant #1044 — a re-export hands another file's OBJECT straight
|
|
2006
|
+
// through under this file's name, so it is a live-identity edge
|
|
2007
|
+
// exactly like an imported binding is (see `liveSources`).
|
|
2008
|
+
if (hasObjectIdentity(value)) liveSources.add(targetPath);
|
|
2009
|
+
applyResolvedValue(exportedName, value, entities, exportedValues);
|
|
1648
2010
|
}
|
|
1649
2011
|
}
|
|
1650
2012
|
|
|
1651
|
-
return { ok: true, entities, exportedValues };
|
|
2013
|
+
return { ok: true, entities, exportedValues, liveSources };
|
|
1652
2014
|
} catch (err) {
|
|
1653
2015
|
// Any unexpected failure degrades to "fall back to run" rather than
|
|
1654
2016
|
// taking discovery down with it — fold is opt-in, not a new failure mode.
|
|
@@ -1725,6 +2087,21 @@ export async function tryFoldFile(
|
|
|
1725
2087
|
* the discovered files' relative-import graph, seeded from every file that
|
|
1726
2088
|
* doesn't fold on its own.
|
|
1727
2089
|
*
|
|
2090
|
+
* chant #1044 adds the OTHER half of the same hazard, in the opposite
|
|
2091
|
+
* direction along the same edges. Forward taint covers "a run file imports a
|
|
2092
|
+
* folded file"; it does not cover "a FOLDED file consumed the objects of a
|
|
2093
|
+
* file that later got forced to run". Once a plain-call intrinsic can fold,
|
|
2094
|
+
* that second case is easy to reach: in `lexicons/aws/examples/lambda-api`,
|
|
2095
|
+
* `health-api.ts` folds and captures `params.ts`'s real `Parameter` instance
|
|
2096
|
+
* through `Ref(environment)`, while `params.ts` itself is forced to run
|
|
2097
|
+
* because a DIFFERENT sibling (`data-bucket.ts`) imports it and falls back.
|
|
2098
|
+
* Discovery then collects the run instance and serializes the folded one —
|
|
2099
|
+
* the same "Logical name not set" crash described above, arriving from the
|
|
2100
|
+
* other side. So `liveSources` (see {@link FoldFileResult}) contributes
|
|
2101
|
+
* reverse edges here: a tainted file taints every folded file that captured
|
|
2102
|
+
* one of its objects. Only object identity propagates — a file that imported
|
|
2103
|
+
* a plain string from a tainted file has nothing to disagree about.
|
|
2104
|
+
*
|
|
1728
2105
|
* chant #1020 changes the calculus but not this function: `alb.ts` can now
|
|
1729
2106
|
* often fold `network.vpc.VpcId` too (see `buildExternals`/`foldFileMemoized`
|
|
1730
2107
|
* above), by reusing THE EXACT SAME `tryFoldFile("network.ts")` call (memoized
|
|
@@ -1740,6 +2117,7 @@ export async function tryFoldFile(
|
|
|
1740
2117
|
export async function planFoldTaint(
|
|
1741
2118
|
files: readonly string[],
|
|
1742
2119
|
wouldFold: ReadonlyMap<string, boolean>,
|
|
2120
|
+
liveSources?: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1743
2121
|
): Promise<Set<string>> {
|
|
1744
2122
|
const fileSet = new Set(files);
|
|
1745
2123
|
|
|
@@ -1789,6 +2167,22 @@ export async function planFoldTaint(
|
|
|
1789
2167
|
edges.set(file, targets);
|
|
1790
2168
|
}
|
|
1791
2169
|
|
|
2170
|
+
// chant #1044 — reverse edges: consumed-file -> the folded files that
|
|
2171
|
+
// captured its objects. Same taint set, same fixpoint walk; see this
|
|
2172
|
+
// function's doc for the crash this closes.
|
|
2173
|
+
for (const [consumer, sources] of liveSources ?? []) {
|
|
2174
|
+
if (!fileSet.has(consumer)) continue;
|
|
2175
|
+
for (const source of sources) {
|
|
2176
|
+
if (!fileSet.has(source)) continue;
|
|
2177
|
+
let back = edges.get(source);
|
|
2178
|
+
if (!back) {
|
|
2179
|
+
back = new Set<string>();
|
|
2180
|
+
edges.set(source, back);
|
|
2181
|
+
}
|
|
2182
|
+
back.add(consumer);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
1792
2186
|
const tainted = new Set<string>(files.filter((f) => wouldFold.get(f) !== true));
|
|
1793
2187
|
const queue = [...tainted];
|
|
1794
2188
|
while (queue.length > 0) {
|