@intentius/chant 0.21.0 → 0.23.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/build-params-cli.d.ts +55 -0
- package/dist/cli/build-params-cli.d.ts.map +1 -0
- 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/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +14 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/lsp/server.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +33 -2
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +28 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +71 -9
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +27 -4
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +39 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +11 -1
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +14 -0
- package/dist/lint/rule.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/build-params-cli.test.ts +139 -0
- package/src/cli/build-params-cli.ts +107 -0
- package/src/cli/commands/build.ts +25 -36
- package/src/cli/commands/check-lexicon-examples.ts +16 -1
- package/src/cli/commands/lint.test.ts +74 -0
- package/src/cli/commands/lint.ts +33 -9
- package/src/cli/handlers/build.test.ts +147 -0
- package/src/cli/handlers/build.ts +23 -8
- package/src/cli/handlers/run.test.ts +160 -5
- package/src/cli/handlers/run.ts +46 -8
- package/src/cli/lsp/server.ts +7 -2
- package/src/components/cli-support.test.ts +221 -3
- package/src/components/cli-support.ts +37 -6
- package/src/components/discover.test.ts +63 -1
- package/src/components/discover.ts +42 -0
- package/src/discovery/fold-import.test.ts +328 -1
- package/src/discovery/fold-import.ts +414 -31
- package/src/discovery/index.test.ts +131 -0
- package/src/discovery/index.ts +53 -8
- package/src/discovery/sandbox/fold-boundary.test.ts +254 -0
- package/src/fold/fold.ts +6 -2
- package/src/fold/subset.test.ts +95 -15
- package/src/fold/subset.ts +46 -5
- package/src/lint/engine.ts +12 -0
- package/src/lint/rule.ts +14 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +39 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -1
- package/src/serializer-walker.ts +14 -0
|
@@ -7,6 +7,7 @@ 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,
|
|
@@ -19,7 +20,8 @@ import {
|
|
|
19
20
|
type FoldedHelperCall,
|
|
20
21
|
type SymbolicValue,
|
|
21
22
|
} from "../fold/fold";
|
|
22
|
-
import { isChantOwnedSpecifier } from "../fold/foldable-helpers";
|
|
23
|
+
import { isChantOwnedSpecifier, isFoldableHelperName } from "../fold/foldable-helpers";
|
|
24
|
+
import { briefNodeText, callExpressionMessage } from "../fold/subset";
|
|
23
25
|
import { importModule } from "./import";
|
|
24
26
|
import type { IntrinsicDef } from "../lexicon";
|
|
25
27
|
import type { BuildParamValue } from "../build-params";
|
|
@@ -72,16 +74,29 @@ export type FoldedEntity = [name: string, entity: Declarable | CompositeInstance
|
|
|
72
74
|
export type FoldFileResult =
|
|
73
75
|
| {
|
|
74
76
|
ok: true;
|
|
77
|
+
/**
|
|
78
|
+
* The `Declarable`/`CompositeInstance` subset of {@link
|
|
79
|
+
* FoldFileResult.exportedValues} — how many resources this file
|
|
80
|
+
* contributed, for the `[fold:fold] x.ts — N resource(s)` decision
|
|
81
|
+
* line. chant #1112: NOT what discovery collects from. Collection
|
|
82
|
+
* reads `exportedValues`, so that `./collect.ts` stays the single
|
|
83
|
+
* owner of which exports become entities — see {@link
|
|
84
|
+
* applyResolvedValue}.
|
|
85
|
+
*/
|
|
75
86
|
entities: FoldedEntity[];
|
|
76
87
|
/**
|
|
77
88
|
* chant #1020 — EVERY exported name's fully-resolved value, not just
|
|
78
|
-
* the `Declarable`/`CompositeInstance` ones
|
|
79
|
-
* plain value folds too (a string, a number, a plain object)
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
89
|
+
* the `Declarable`/`CompositeInstance` ones also listed in `entities`:
|
|
90
|
+
* a plain value folds too (a string, a number, a plain object). A
|
|
91
|
+
* successful fold means `scanExports` recognized every export the file
|
|
92
|
+
* has (anything else disqualifies the whole file), so this IS the
|
|
93
|
+
* file's complete export namespace — the same table the run path gets
|
|
94
|
+
* from actually importing it. Two consumers: `discover()` passes it
|
|
95
|
+
* straight to `collectEntities` (chant #1112), and another file's
|
|
96
|
+
* cross-file reference resolves against it — see `buildExternals`
|
|
97
|
+
* below and the module doc on `planFoldTaint` for why a
|
|
98
|
+
* resource/composite value here MUST be the exact same object every
|
|
99
|
+
* referencing file sees.
|
|
85
100
|
*/
|
|
86
101
|
exportedValues: Map<string, unknown>;
|
|
87
102
|
/**
|
|
@@ -171,14 +186,77 @@ export interface FoldSession {
|
|
|
171
186
|
* doesn't use build-time parameters pays nothing extra here.
|
|
172
187
|
*/
|
|
173
188
|
readonly buildParams?: Readonly<Record<string, BuildParamValue>>;
|
|
189
|
+
/**
|
|
190
|
+
* chant #1063 — the exact package specifiers of the lexicons LOADED for
|
|
191
|
+
* this build (`@intentius/chant-lexicon-aws`, …), derived from the lexicon
|
|
192
|
+
* names the build already resolved (`resolveProjectLexicons` ->
|
|
193
|
+
* `loadPlugins`, see ../cli/plugins.ts). This is the entire allowlist
|
|
194
|
+
* {@link buildExternals} will follow a bare import specifier into — see
|
|
195
|
+
* {@link activeLexiconPackage} for why the set is matched by TEXT and
|
|
196
|
+
* built from names the build already knows, rather than by resolving
|
|
197
|
+
* specifiers to find out what they are.
|
|
198
|
+
*
|
|
199
|
+
* Empty when the caller supplied no lexicon list, which disables
|
|
200
|
+
* lexicon-package resolution entirely rather than falling back to
|
|
201
|
+
* something more permissive: "an active lexicon of this build" is the
|
|
202
|
+
* boundary, and a build that can't say what its lexicons are hasn't
|
|
203
|
+
* established one.
|
|
204
|
+
*/
|
|
205
|
+
readonly lexiconPackages: ReadonlySet<string>;
|
|
206
|
+
/**
|
|
207
|
+
* chant #1093 — this build asked for the #1045 sandbox
|
|
208
|
+
* (`DiscoveryOptions.sandbox`, `chant build --sandbox`), so fold must not
|
|
209
|
+
* import or invoke a module the CLI process isn't already trusted to
|
|
210
|
+
* execute. See {@link isTrustedExecutableBinding} for what that allowlist
|
|
211
|
+
* is and {@link sandboxedExecutionRefusal} for what happens at each site
|
|
212
|
+
* that would otherwise execute one.
|
|
213
|
+
*
|
|
214
|
+
* `false` (the default, plain `--fold`) leaves every resolution path
|
|
215
|
+
* exactly as it was: fold already trusts the code enough to fall back to
|
|
216
|
+
* an in-process `importModule` when it can't fold something, so gating
|
|
217
|
+
* only the fold half would buy nothing there.
|
|
218
|
+
*/
|
|
219
|
+
readonly sandbox: boolean;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* chant #1063 — the package specifier a lexicon NAME (`"aws"`, `"gitlab"`)
|
|
224
|
+
* is installed under. The one naming convention the whole CLI already
|
|
225
|
+
* depends on: `loadPlugin(name)` imports exactly this
|
|
226
|
+
* (../cli/plugins.ts), `detectLexicons` scans source for exactly this
|
|
227
|
+
* (../detectLexicon.ts), and `chant init` writes exactly this into
|
|
228
|
+
* package.json.
|
|
229
|
+
*/
|
|
230
|
+
export function lexiconPackageName(lexiconName: string): string {
|
|
231
|
+
return `@intentius/chant-lexicon-${lexiconName}`;
|
|
174
232
|
}
|
|
175
233
|
|
|
176
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Create a fresh, empty {@link FoldSession}.
|
|
236
|
+
*
|
|
237
|
+
* @param lexicons - chant #1063: the lexicon NAMES active for this build
|
|
238
|
+
* (`["aws", "k8s"]`). Converted to package specifiers via
|
|
239
|
+
* {@link lexiconPackageName}; see {@link FoldSession.lexiconPackages}.
|
|
240
|
+
* @param sandbox - chant #1093: this build asked for the #1045 sandbox, so
|
|
241
|
+
* fold may not import or invoke anything outside the trusted allowlist —
|
|
242
|
+
* see {@link FoldSession.sandbox}.
|
|
243
|
+
*/
|
|
177
244
|
export function createFoldSession(
|
|
178
245
|
intrinsics: readonly IntrinsicDef[] = [],
|
|
179
246
|
buildParams?: Readonly<Record<string, BuildParamValue>>,
|
|
247
|
+
lexicons: readonly string[] = [],
|
|
248
|
+
sandbox = false,
|
|
180
249
|
): FoldSession {
|
|
181
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
intrinsics,
|
|
252
|
+
cache: new Map(),
|
|
253
|
+
stack: [],
|
|
254
|
+
importCache: new Map(),
|
|
255
|
+
resolvePathCache: new Map(),
|
|
256
|
+
buildParams,
|
|
257
|
+
lexiconPackages: new Set(lexicons.map(lexiconPackageName)),
|
|
258
|
+
sandbox,
|
|
259
|
+
};
|
|
182
260
|
}
|
|
183
261
|
|
|
184
262
|
/**
|
|
@@ -805,6 +883,100 @@ function paramsModulePath(): string | null {
|
|
|
805
883
|
return paramsModulePathMemo;
|
|
806
884
|
}
|
|
807
885
|
|
|
886
|
+
/**
|
|
887
|
+
* chant #1063 — is `specifier` a bare import of a package that is an ACTIVE
|
|
888
|
+
* LEXICON of this build? Returns the specifier itself when so, `undefined`
|
|
889
|
+
* otherwise.
|
|
890
|
+
*
|
|
891
|
+
* Three deliberate restrictions, each of which is the point rather than an
|
|
892
|
+
* omission:
|
|
893
|
+
*
|
|
894
|
+
* - **Text only, no resolution.** The answer is a `Set.has` on a set built
|
|
895
|
+
* from the lexicon names the build ALREADY resolved before discovery ran
|
|
896
|
+
* (`resolveProjectLexicons` -> `loadPlugins`). Nothing is probed, read, or
|
|
897
|
+
* resolved to decide whether a specifier is in scope — so an import of
|
|
898
|
+
* some unrelated package costs a string comparison and is then left alone,
|
|
899
|
+
* never resolved "just to find out what it is". That matters here more
|
|
900
|
+
* than anywhere: `resolveModulePath`'s bare branch can fall through to
|
|
901
|
+
* `createRequire(fromFile).resolve(specifier)`, measured at up to ~361s
|
|
902
|
+
* for the first resolution of a genuinely new bare specifier in a process
|
|
903
|
+
* (see {@link bareSpecifierPathCache}/{@link fastResolveBareSpecifier} —
|
|
904
|
+
* this exact class of cost regressed twice during chant#1020).
|
|
905
|
+
*
|
|
906
|
+
* - **Exact package specifier, no subpaths.** `@intentius/chant-lexicon-aws`
|
|
907
|
+
* matches; `@intentius/chant-lexicon-aws/actions` does not. Every lexicon
|
|
908
|
+
* re-exports its whole public surface from its barrel and every example
|
|
909
|
+
* imports it that way, so subpaths buy nothing — and they would cost
|
|
910
|
+
* something real: {@link fastResolveBareSpecifier} only recognizes a
|
|
911
|
+
* package ROOT (it looks for `<node_modules>/<specifier>/package.json`),
|
|
912
|
+
* so a subpath specifier falls through to exactly the slow
|
|
913
|
+
* `createRequire().resolve()` path this restriction exists to avoid.
|
|
914
|
+
*
|
|
915
|
+
* - **Lexicons of THIS build only.** Not "any `@intentius/chant-lexicon-*`
|
|
916
|
+
* package on disk", and emphatically not "any bare specifier". A lexicon
|
|
917
|
+
* the build did not load is as out of scope as `node:fs`.
|
|
918
|
+
*/
|
|
919
|
+
function activeLexiconPackage(specifier: string, lexiconPackages: ReadonlySet<string>): string | undefined {
|
|
920
|
+
return lexiconPackages.has(specifier) ? specifier : undefined;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* chant #1063 — resolve one named import binding against an active lexicon
|
|
925
|
+
* package's REAL exports, for {@link buildExternals}.
|
|
926
|
+
*
|
|
927
|
+
* The lexicon module is imported (through the session-wide
|
|
928
|
+
* {@link FoldSession.importCache}, so at most one real `import()` per package
|
|
929
|
+
* per build) and the requested export read straight off it. That is the same
|
|
930
|
+
* module object the run path gets — `importModule` is a plain dynamic
|
|
931
|
+
* `import()` with no cache-busting — so what fold captures here is not a
|
|
932
|
+
* reconstruction of the lexicon's data but the identical value, down to
|
|
933
|
+
* object identity for `Azure.ResourceGroupLocation`-style singletons. It is
|
|
934
|
+
* also the same two-step (resolve path, then import) that
|
|
935
|
+
* `resolveImportedExport` has always used to reach a lexicon's constructors
|
|
936
|
+
* and intrinsic functions; the only thing new is that a plain DATA export is
|
|
937
|
+
* now reachable too.
|
|
938
|
+
*
|
|
939
|
+
* Callable exports are deliberately excluded. A lexicon's functions — its
|
|
940
|
+
* resource classes, composite factories, intrinsic implementations — already
|
|
941
|
+
* have dedicated resolution paths that know how to INVOKE them
|
|
942
|
+
* (`resolveResourceEntity`, `resolveCallExpression`, `reviveFoldedValue`), and
|
|
943
|
+
* binding them as plain identifier values here would widen what folds in ways
|
|
944
|
+
* this issue neither needs nor measured: none of the references #1063 exists
|
|
945
|
+
* to unblock (`Azure`, `GCP`, `S3Actions`, gitlab's `CI`) is a function.
|
|
946
|
+
*
|
|
947
|
+
* Returns `undefined` — never throws — for a package that isn't an active
|
|
948
|
+
* lexicon, an export the package doesn't have, a callable export, or an
|
|
949
|
+
* import that fails. The binding is then simply absent from `externals`,
|
|
950
|
+
* exactly as before, and `fold()`'s ordinary "unresolved identifier" failure
|
|
951
|
+
* still fires if the name is actually referenced.
|
|
952
|
+
*/
|
|
953
|
+
async function resolveActiveLexiconExport(
|
|
954
|
+
binding: ImportBinding,
|
|
955
|
+
fromFile: string,
|
|
956
|
+
session: FoldSession,
|
|
957
|
+
): Promise<{ value: unknown } | undefined> {
|
|
958
|
+
if (!activeLexiconPackage(binding.specifier, session.lexiconPackages)) return undefined;
|
|
959
|
+
|
|
960
|
+
let modulePath: string;
|
|
961
|
+
try {
|
|
962
|
+
modulePath = resolveModulePathMemoized(binding.specifier, fromFile, session.resolvePathCache);
|
|
963
|
+
} catch {
|
|
964
|
+
return undefined;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
let mod: Record<string, unknown>;
|
|
968
|
+
try {
|
|
969
|
+
mod = await importModuleMemoized(modulePath, session.importCache);
|
|
970
|
+
} catch {
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!(binding.imported in mod)) return undefined;
|
|
975
|
+
const value = mod[binding.imported];
|
|
976
|
+
if (typeof value === "function") return undefined;
|
|
977
|
+
return { value };
|
|
978
|
+
}
|
|
979
|
+
|
|
808
980
|
// ─────────────────────────────────────────────────────────────────────────
|
|
809
981
|
// Resolution: given the scan + import map, compute the REAL runtime value
|
|
810
982
|
// (Declarable | CompositeInstance) each foldable export would have had if
|
|
@@ -867,6 +1039,16 @@ interface ResolveCtx {
|
|
|
867
1039
|
* {@link resolveModulePathMemoized} with this map.
|
|
868
1040
|
*/
|
|
869
1041
|
resolvePathCache: Map<string, string>;
|
|
1042
|
+
/**
|
|
1043
|
+
* chant #1063 — this build's active lexicon PACKAGE specifiers (see
|
|
1044
|
+
* {@link FoldSession.lexiconPackages}). Threaded down here, not just used
|
|
1045
|
+
* in `buildExternals`, because chant #1093's trust check
|
|
1046
|
+
* ({@link isTrustedExecutableBinding}) needs the same allowlist at every
|
|
1047
|
+
* site that imports and executes a module.
|
|
1048
|
+
*/
|
|
1049
|
+
lexiconPackages: ReadonlySet<string>;
|
|
1050
|
+
/** chant #1093 — see {@link FoldSession.sandbox}. */
|
|
1051
|
+
sandbox: boolean;
|
|
870
1052
|
}
|
|
871
1053
|
|
|
872
1054
|
/** `{ 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. */
|
|
@@ -962,15 +1144,28 @@ async function resolveLiveValue(node: ts.Expression, ctx: ResolveCtx): Promise<L
|
|
|
962
1144
|
* `isDeclarable`/`isCompositeInstance` value for a top-level export).
|
|
963
1145
|
*/
|
|
964
1146
|
async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx): Promise<unknown> {
|
|
1147
|
+
// chant #1054 — reuses ../fold/subset's `callExpressionMessage` (the SAME
|
|
1148
|
+
// builder `fold()` throws with for a call used as a prop value) rather
|
|
1149
|
+
// than a hand-written "call expression as a value" copy: the two used to
|
|
1150
|
+
// say different things for the identical rejection, which silently broke
|
|
1151
|
+
// any tooling grouping fallback reasons by text.
|
|
965
1152
|
if (!ts.isIdentifier(node.expression)) {
|
|
966
|
-
throw cheapError(
|
|
1153
|
+
throw cheapError(callExpressionMessage(node));
|
|
967
1154
|
}
|
|
968
1155
|
const calleeName = node.expression.text;
|
|
969
1156
|
const binding = ctx.imports.get(calleeName);
|
|
970
1157
|
if (!binding) {
|
|
971
|
-
throw cheapError(
|
|
1158
|
+
throw cheapError(callExpressionMessage(node));
|
|
972
1159
|
}
|
|
973
1160
|
|
|
1161
|
+
// chant #1093 — THE gap this check exists for. Invoking the callee runs
|
|
1162
|
+
// project code (the factory body, and its whole module's top level) in the
|
|
1163
|
+
// CLI's own process; under --sandbox that has to happen in the child
|
|
1164
|
+
// instead, so refuse here and let the file fall back to the sandboxed run
|
|
1165
|
+
// path. See {@link sandboxedExecutionRefusal}.
|
|
1166
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, calleeName, "composite factory");
|
|
1167
|
+
if (refusal) throw cheapError(refusal);
|
|
1168
|
+
|
|
974
1169
|
let modulePath: string;
|
|
975
1170
|
try {
|
|
976
1171
|
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
@@ -1007,6 +1202,22 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1007
1202
|
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a function`);
|
|
1008
1203
|
}
|
|
1009
1204
|
|
|
1205
|
+
// chant #1112 — ONE rule for a registered authoring helper's arguments,
|
|
1206
|
+
// applied at BOTH sites that can invoke one. `reviveHelperCall` (the
|
|
1207
|
+
// nested-value site, #1082) already revives a helper's arguments with
|
|
1208
|
+
// `requireLiveRefs`, because a helper reads THROUGH its ref (`output()`
|
|
1209
|
+
// derefs the `WeakRef` parent) and a look-alike `{__attrRef}` envelope
|
|
1210
|
+
// makes it produce a wrong result rather than none. This site — a
|
|
1211
|
+
// top-level `export const oArn = output(bucket.Arn, "oArn")` — took the
|
|
1212
|
+
// composite-factory rule (`false`) instead, which is right for a factory
|
|
1213
|
+
// (its props keep the envelope, and the serializer's own walker resolves
|
|
1214
|
+
// it) and wrong for a helper. It went unnoticed while the resulting
|
|
1215
|
+
// `LexiconOutput` was being discarded anyway; with the export namespace
|
|
1216
|
+
// now collected in full, a same-file `output(...)` would reach the
|
|
1217
|
+
// serializer holding an inert envelope where the run path has a real
|
|
1218
|
+
// reference. Rejected here instead, which falls the file back to run —
|
|
1219
|
+
// absent output, never a wrong one.
|
|
1220
|
+
const helperArgs = isFoldableHelperName(calleeName);
|
|
1010
1221
|
const args: unknown[] = [];
|
|
1011
1222
|
for (const argNode of node.arguments) {
|
|
1012
1223
|
const live = await resolveLiveValue(argNode, ctx);
|
|
@@ -1017,7 +1228,7 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1017
1228
|
args.push(
|
|
1018
1229
|
live !== undefined
|
|
1019
1230
|
? live.value
|
|
1020
|
-
: await reviveFoldedValue(fold(argNode, ctx.consts, ctx.intrinsics, ctx.externals), ctx,
|
|
1231
|
+
: await reviveFoldedValue(fold(argNode, ctx.consts, ctx.intrinsics, ctx.externals), ctx, helperArgs),
|
|
1021
1232
|
);
|
|
1022
1233
|
}
|
|
1023
1234
|
|
|
@@ -1025,17 +1236,23 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1025
1236
|
}
|
|
1026
1237
|
|
|
1027
1238
|
/**
|
|
1028
|
-
* Record one exported name's fully-resolved value: into
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
* `
|
|
1038
|
-
*
|
|
1239
|
+
* Record one exported name's fully-resolved value: unconditionally into
|
|
1240
|
+
* `exportedValues` (chant #1020) — the file's export namespace, which is
|
|
1241
|
+
* what discovery hands to `collectEntities` and what another file's
|
|
1242
|
+
* cross-file reference resolves against — and, additionally, into `entities`
|
|
1243
|
+
* when the value is a real `Declarable`/`CompositeInstance`.
|
|
1244
|
+
*
|
|
1245
|
+
* chant #1112 — `entities` is a REPORTING subset, not a filter. It used to
|
|
1246
|
+
* be the only thing discovery passed on, which made this function a second
|
|
1247
|
+
* owner of the "which export becomes an entity" decision; it had one fewer
|
|
1248
|
+
* case than the real owner (`enumerateEntries`, ../collect.ts), so a
|
|
1249
|
+
* `LexiconOutput` export folded fine and was then thrown away, and the
|
|
1250
|
+
* template silently lost its `Outputs` section. Discovery now passes
|
|
1251
|
+
* `exportedValues` — the whole namespace, exactly like the run path's real
|
|
1252
|
+
* `exports` object — and `collectEntities` filters it, so nothing here can
|
|
1253
|
+
* fall behind what collection understands. What `entities` still answers is
|
|
1254
|
+
* "how many resources did this file contribute", for the `[fold:fold] x.ts —
|
|
1255
|
+
* N resource(s)` decision line.
|
|
1039
1256
|
*/
|
|
1040
1257
|
function applyResolvedValue(
|
|
1041
1258
|
name: string,
|
|
@@ -1089,6 +1306,14 @@ async function resolveImportedExport(name: string, ctx: ResolveCtx): Promise<unk
|
|
|
1089
1306
|
throw cheapError(`"${name}" is not a resolvable import`);
|
|
1090
1307
|
}
|
|
1091
1308
|
|
|
1309
|
+
// chant #1093 — reached for an intrinsic tag and for a symbolic chain's root
|
|
1310
|
+
// (`AWS.StackName`), both of which are resolved BY NAME out of the file's own
|
|
1311
|
+
// imports: nothing guarantees the module behind that name is a lexicon's.
|
|
1312
|
+
// `reviveHelperCall` checks chant-ownership before it gets here; this covers
|
|
1313
|
+
// the paths that don't.
|
|
1314
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, name, "import");
|
|
1315
|
+
if (refusal) throw cheapError(refusal);
|
|
1316
|
+
|
|
1092
1317
|
let modulePath: string;
|
|
1093
1318
|
try {
|
|
1094
1319
|
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
@@ -1161,7 +1386,17 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, requireLiv
|
|
|
1161
1386
|
// destroying the very identity #1020 exists to preserve. Passed through
|
|
1162
1387
|
// completely unchanged, exactly like `resolveCallExpression`'s own
|
|
1163
1388
|
// `live.value` passthrough for a composite-call argument.
|
|
1164
|
-
|
|
1389
|
+
// chant #1063 adds the third kind of real object cross-file resolution can
|
|
1390
|
+
// now put here: a live `Intrinsic` instance read off an active lexicon
|
|
1391
|
+
// package (`Azure.ResourceGroupLocation`, `GCP.ProjectId` — see
|
|
1392
|
+
// `resolveActiveLexiconExport`). Same hazard, same fix: the generic walk
|
|
1393
|
+
// below would rebuild it as a plain `{}` copy, dropping the prototype that
|
|
1394
|
+
// carries `toJSON()` and so serializing `{}` where the run path emits
|
|
1395
|
+
// `[resourceGroup().location]`. `isIntrinsic` keys off
|
|
1396
|
+
// `Symbol.for("chant.intrinsic")` (../intrinsic.ts), a GLOBAL symbol, so it
|
|
1397
|
+
// holds across separately-loaded copies of chant-core the way a bare
|
|
1398
|
+
// `instanceof` would not.
|
|
1399
|
+
if (isAttrRefLike(value) || isDeclarable(value) || isCompositeInstance(value) || isIntrinsic(value)) {
|
|
1165
1400
|
return value;
|
|
1166
1401
|
}
|
|
1167
1402
|
|
|
@@ -1279,6 +1514,15 @@ async function reviveHelperCall(call: FoldedHelperCall, ctx: ResolveCtx): Promis
|
|
|
1279
1514
|
* pathological cold-resolution cost chant#1020 measured (see
|
|
1280
1515
|
* {@link fastResolveBareSpecifier}), and a bare specifier chant publishes is
|
|
1281
1516
|
* already covered by the text arm.
|
|
1517
|
+
*
|
|
1518
|
+
* Not the same question as chant#1093's {@link isTrustedExecutableBinding}
|
|
1519
|
+
* below, and deliberately not shared with it: this one asks "may this NAME be
|
|
1520
|
+
* invoked as one of chant's registered authoring helpers", and a text match
|
|
1521
|
+
* is the right answer for it — a project that shadows `@intentius/chant` in
|
|
1522
|
+
* its own `node_modules` gets its own copy of `output()` invoked either way,
|
|
1523
|
+
* fold or run, so fold cannot diverge from run by trusting the text here.
|
|
1524
|
+
* #1093's question is "may this module execute in the CLI's process at all",
|
|
1525
|
+
* where a specifier the project controls the text of proves nothing.
|
|
1282
1526
|
*/
|
|
1283
1527
|
function isChantOwnedHelperBinding(binding: ImportBinding, ctx: ResolveCtx): boolean {
|
|
1284
1528
|
if (isChantOwnedSpecifier(binding.specifier)) return true;
|
|
@@ -1293,6 +1537,89 @@ function isChantOwnedHelperBinding(binding: ImportBinding, ctx: ResolveCtx): boo
|
|
|
1293
1537
|
return targetPath === root || targetPath.startsWith(root + sep);
|
|
1294
1538
|
}
|
|
1295
1539
|
|
|
1540
|
+
/**
|
|
1541
|
+
* chant #1093 — the closed allowlist of modules fold may import AND EXECUTE
|
|
1542
|
+
* in the CLI's own process when the #1045 sandbox is active. Exactly two
|
|
1543
|
+
* arms, and neither of them trusts text the project controls:
|
|
1544
|
+
*
|
|
1545
|
+
* 1. **An ACTIVE lexicon package of this build** — matched against
|
|
1546
|
+
* {@link FoldSession.lexiconPackages}, a closed set built from the
|
|
1547
|
+
* lexicon names the BUILD resolved and `loadPlugins` already imported
|
|
1548
|
+
* (../cli/plugins.ts), not from anything the file under fold says.
|
|
1549
|
+
* 2. **chant-core's own executing tree** — the specifier is RESOLVED and the
|
|
1550
|
+
* resulting path checked against {@link chantCoreRoot}. A text match is
|
|
1551
|
+
* not enough here: `@intentius/chant` and `@intentius/chant-lexicon-evil`
|
|
1552
|
+
* are both strings an untrusted repo can write into its own source and
|
|
1553
|
+
* back with its own `node_modules` directory, and
|
|
1554
|
+
* {@link isChantOwnedSpecifier} would accept either. Resolution happens
|
|
1555
|
+
* before the decision rather than after, so an allowed binding pays
|
|
1556
|
+
* exactly the resolution it was about to pay anyway (the memoized one —
|
|
1557
|
+
* see {@link resolveModulePathMemoized}); a bare specifier that isn't
|
|
1558
|
+
* even chant-shaped is rejected on text alone, so no arbitrary bare
|
|
1559
|
+
* specifier is ever resolved here (chant#1020's cold-resolution cost).
|
|
1560
|
+
*
|
|
1561
|
+
* This is deliberately the boundary chant #1045 drew: "the boundary is around
|
|
1562
|
+
* executing PROJECT SOURCE, which is the untrusted input" — not around chant
|
|
1563
|
+
* itself or the lexicon packages, which the CLI has already imported and
|
|
1564
|
+
* executed in its own process before discovery starts, to get the serializers
|
|
1565
|
+
* and lint rules it cannot run without. Fold reaching the same already-loaded
|
|
1566
|
+
* module (the identical un-cache-busted `import()` of the identical resolved
|
|
1567
|
+
* path) adds no execution the process wasn't already performing.
|
|
1568
|
+
*
|
|
1569
|
+
* Everything else is out: a sibling project file, a project-local helper
|
|
1570
|
+
* module, an arbitrary npm dependency, a lexicon this build didn't load. A
|
|
1571
|
+
* build that supplied no lexicon list keeps only arm 2, rather than falling
|
|
1572
|
+
* back to something more permissive — the same stance
|
|
1573
|
+
* {@link FoldSession.lexiconPackages} takes for #1063.
|
|
1574
|
+
*/
|
|
1575
|
+
function isTrustedExecutableBinding(binding: ImportBinding, ctx: ResolveCtx): boolean {
|
|
1576
|
+
if (activeLexiconPackage(binding.specifier, ctx.lexiconPackages) !== undefined) return true;
|
|
1577
|
+
// Only a chant-shaped or project-relative specifier is worth resolving; any
|
|
1578
|
+
// other bare specifier is untrusted by definition, and resolving it to find
|
|
1579
|
+
// that out would cost the pathological cold `require.resolve` (chant#1020).
|
|
1580
|
+
if (!isProjectFileSpecifier(binding.specifier) && !isChantOwnedSpecifier(binding.specifier)) return false;
|
|
1581
|
+
let targetPath: string;
|
|
1582
|
+
try {
|
|
1583
|
+
targetPath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
1584
|
+
} catch {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
const root = chantCoreRoot();
|
|
1588
|
+
return targetPath === root || targetPath.startsWith(root + sep);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* chant #1093 — the one-line fold-fallback reason for a resolution that would
|
|
1593
|
+
* import and execute an untrusted module in the CLI's own process, or
|
|
1594
|
+
* `undefined` when the import may proceed (the sandbox wasn't asked for, or
|
|
1595
|
+
* the module is on {@link isTrustedExecutableBinding}'s allowlist).
|
|
1596
|
+
*
|
|
1597
|
+
* A refusal is not a failure to fold something folder-shaped — the shape is
|
|
1598
|
+
* perfectly foldable and folds fine under plain `--fold`. It is a deliberate
|
|
1599
|
+
* demotion: the file falls back to the run path, and under `--sandbox` the
|
|
1600
|
+
* run path is the sandboxed child (`./index.ts` queues every run-fallback
|
|
1601
|
+
* file for `./sandbox/run.ts`). So the factory/constructor/intrinsic still
|
|
1602
|
+
* executes, with the same arguments, in the same module graph as the rest of
|
|
1603
|
+
* that file — just behind Node's Permission Model and a scrubbed environment
|
|
1604
|
+
* instead of inside the CLI. Coverage drops; the boundary holds.
|
|
1605
|
+
*
|
|
1606
|
+
* @param what - What the binding is being resolved AS, for the message
|
|
1607
|
+
* ("composite factory", "constructor", …).
|
|
1608
|
+
*/
|
|
1609
|
+
function sandboxedExecutionRefusal(
|
|
1610
|
+
binding: ImportBinding,
|
|
1611
|
+
ctx: ResolveCtx,
|
|
1612
|
+
name: string,
|
|
1613
|
+
what: string,
|
|
1614
|
+
): string | undefined {
|
|
1615
|
+
if (!ctx.sandbox) return undefined;
|
|
1616
|
+
if (isTrustedExecutableBinding(binding, ctx)) return undefined;
|
|
1617
|
+
return (
|
|
1618
|
+
`${what} "${name}" is imported from "${binding.specifier}", which is neither chant's own nor an active lexicon — ` +
|
|
1619
|
+
`under --sandbox it is executed in the sandboxed child, not in this process`
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1296
1623
|
/**
|
|
1297
1624
|
* chant-core's own module root — `packages/core/src` in this repo,
|
|
1298
1625
|
* `<pkg>/dist` in a published install — derived from THIS module's location.
|
|
@@ -1378,6 +1705,14 @@ async function resolveResourceEntity(
|
|
|
1378
1705
|
return { ok: false, reason: `constructor "${typeName}" for "${name}" is not a resolvable import` };
|
|
1379
1706
|
}
|
|
1380
1707
|
|
|
1708
|
+
// chant #1093 — a resource class is a lexicon export in every corpus entry
|
|
1709
|
+
// today, but nothing forces that: `new Thing(...)` where `Thing` comes from
|
|
1710
|
+
// a project file (or an arbitrary dependency) would import and run that
|
|
1711
|
+
// module here, in the CLI's process. Same refusal as the composite-factory
|
|
1712
|
+
// path above.
|
|
1713
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, typeName, "constructor");
|
|
1714
|
+
if (refusal) return { ok: false, reason: refusal };
|
|
1715
|
+
|
|
1381
1716
|
let modulePath: string;
|
|
1382
1717
|
try {
|
|
1383
1718
|
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
@@ -1443,6 +1778,18 @@ function describeFoldFailure(err: unknown, ctx: ResolveCtx): string {
|
|
|
1443
1778
|
return err.message;
|
|
1444
1779
|
}
|
|
1445
1780
|
|
|
1781
|
+
/**
|
|
1782
|
+
* chant #1054 — a short, single-line label for a destructured export's
|
|
1783
|
+
* source expression, for a fold fallback reason: the callee plus `(...)`
|
|
1784
|
+
* for the common composite-call source (`GkeCluster(...)`), or a brief,
|
|
1785
|
+
* bounded rendering of whatever else it is otherwise. Never the source's own
|
|
1786
|
+
* `getText()` — for a real composite call that's the entire multi-line
|
|
1787
|
+
* argument list.
|
|
1788
|
+
*/
|
|
1789
|
+
function describeDestructureSource(node: ts.Expression): string {
|
|
1790
|
+
return ts.isCallExpression(node) ? `${briefNodeText(node.expression)}(...)` : briefNodeText(node);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1446
1793
|
/** 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. */
|
|
1447
1794
|
function locatedMessage(node: ts.Node, message: string): string {
|
|
1448
1795
|
const { line, column } = locate(node);
|
|
@@ -1585,10 +1932,36 @@ async function buildExternals(
|
|
|
1585
1932
|
}
|
|
1586
1933
|
|
|
1587
1934
|
if (!isProjectFileSpecifier(binding.specifier)) {
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1935
|
+
// chant #1063 — a bare specifier naming one of THIS BUILD's active
|
|
1936
|
+
// lexicon packages resolves to that package's real export, so a plain
|
|
1937
|
+
// data export a lexicon publishes (`Azure`/`GCP`'s pseudo-parameter
|
|
1938
|
+
// namespaces, AWS's `S3Actions`, gitlab's `CI`) is an ordinary
|
|
1939
|
+
// identifier value here rather than fold's most common remaining
|
|
1940
|
+
// "unresolved identifier" failure. See
|
|
1941
|
+
// {@link resolveActiveLexiconExport} for the allowlist, the no-cold-
|
|
1942
|
+
// resolution rule, and why callable exports stay out.
|
|
1943
|
+
//
|
|
1944
|
+
// No `liveSources` edge is recorded for what comes back, unlike the
|
|
1945
|
+
// project-file case just below. `liveSources` (chant #1044) exists so
|
|
1946
|
+
// that a folded file which captured ANOTHER FILE's objects is
|
|
1947
|
+
// invalidated when that file is forced back to run — a fold/run
|
|
1948
|
+
// disagreement about identity. A lexicon package has no such duality:
|
|
1949
|
+
// it is not a discovered source file, `planFoldTaint` never considers
|
|
1950
|
+
// it (it filters to the discovered `files` set), it never falls back to
|
|
1951
|
+
// run, and both paths reach it through the identical un-cache-busted
|
|
1952
|
+
// `import()` of the identical resolved path — so the object fold
|
|
1953
|
+
// captures IS the object the run path holds. There is nothing for the
|
|
1954
|
+
// two sides to disagree about, hence nothing to taint.
|
|
1955
|
+
const lexiconExport = await resolveActiveLexiconExport(binding, file, session);
|
|
1956
|
+
if (lexiconExport) {
|
|
1957
|
+
externals.set(localName, lexiconExport.value);
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
// Every other bare specifier (a non-lexicon vendor package, a lexicon
|
|
1961
|
+
// this build didn't load) is left alone here, exactly as before #1064:
|
|
1962
|
+
// it resolves lazily, through the pre-existing `importModule`
|
|
1963
|
+
// mechanism, only once a constructor/composite-factory/intrinsic tag
|
|
1964
|
+
// actually consumes it.
|
|
1592
1965
|
continue;
|
|
1593
1966
|
}
|
|
1594
1967
|
let targetPath: string;
|
|
@@ -1710,6 +2083,8 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
1710
2083
|
crossFileFailures: failures,
|
|
1711
2084
|
importCache: session.importCache,
|
|
1712
2085
|
resolvePathCache: session.resolvePathCache,
|
|
2086
|
+
lexiconPackages: session.lexiconPackages,
|
|
2087
|
+
sandbox: session.sandbox,
|
|
1713
2088
|
};
|
|
1714
2089
|
|
|
1715
2090
|
const entities: FoldedEntity[] = [];
|
|
@@ -1736,18 +2111,26 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
1736
2111
|
|
|
1737
2112
|
if (decl.kind === "destructure") {
|
|
1738
2113
|
let value: unknown;
|
|
2114
|
+
// chant #1054 — identify the destructured export by its BINDING
|
|
2115
|
+
// NAMES and the source's callee (`"cluster, nodePool" (destructured
|
|
2116
|
+
// from GkeCluster(...))`), never `decl.node.getText()`: for a real
|
|
2117
|
+
// composite call that's the entire multi-line source, which buries
|
|
2118
|
+
// the actual error after it and breaks any line-oriented consumer of
|
|
2119
|
+
// `[fold:run]` output.
|
|
2120
|
+
const boundNames = decl.elements.map((el) => el.bindingName).join(", ");
|
|
2121
|
+
const source = describeDestructureSource(decl.node);
|
|
1739
2122
|
try {
|
|
1740
2123
|
value = (await resolveDeclaratorValue(decl.node, ctx)).value;
|
|
1741
2124
|
} catch (err) {
|
|
1742
2125
|
return {
|
|
1743
2126
|
ok: false,
|
|
1744
|
-
reason: `destructured
|
|
2127
|
+
reason: `"${boundNames}" (destructured from ${source}) is not foldable: ${describeFoldFailure(err, ctx)}`,
|
|
1745
2128
|
};
|
|
1746
2129
|
}
|
|
1747
2130
|
if (!isIndexableObject(value)) {
|
|
1748
2131
|
return {
|
|
1749
2132
|
ok: false,
|
|
1750
|
-
reason: `destructured
|
|
2133
|
+
reason: `"${boundNames}" (destructured from ${source}) is not foldable: not a composite call or object`,
|
|
1751
2134
|
};
|
|
1752
2135
|
}
|
|
1753
2136
|
for (const { propKey, bindingName } of decl.elements) {
|