@intentius/chant 0.108.1 → 0.110.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/lifecycle.d.ts.map +1 -1
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +59 -4
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +17 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +49 -4
- package/dist/fold-provenance.d.ts.map +1 -1
- package/dist/import/ir-utils.d.ts.map +1 -1
- package/dist/import/parser.d.ts +8 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/provenance.d.ts +49 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/dist/stack-output.d.ts +5 -0
- package/dist/stack-output.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/import.test.ts +5 -4
- package/src/cli/handlers/lifecycle.test.ts +106 -1
- package/src/cli/handlers/lifecycle.ts +26 -5
- package/src/composite.ts +37 -0
- package/src/discovery/collect.ts +13 -3
- package/src/discovery/fold-composite.test.ts +22 -0
- package/src/discovery/fold-host-composite.test.ts +23 -0
- package/src/discovery/fold-import.ts +324 -30
- package/src/discovery/fold-package-composite.test.ts +14 -1
- package/src/discovery/fold-project-provenance.test.ts +584 -0
- package/src/discovery/param-deps.test.ts +163 -3
- package/src/discovery/param-deps.ts +173 -4
- package/src/fold/subset-public-export.test.ts +38 -0
- package/src/fold/subset.ts +17 -2
- package/src/fold-provenance.test.ts +190 -16
- package/src/fold-provenance.ts +158 -13
- package/src/import/ir-utils.ts +6 -1
- package/src/import/parser.ts +8 -0
- package/src/provenance.ts +59 -2
- package/src/serializer-walker.test.ts +10 -0
- package/src/serializer-walker.ts +17 -1
- package/src/stack-output.ts +6 -1
- package/src/yaml.test.ts +6 -0
- package/src/yaml.ts +4 -0
|
@@ -52,9 +52,14 @@ import {
|
|
|
52
52
|
collectCompositeOrigins,
|
|
53
53
|
collectParamDependencies,
|
|
54
54
|
collectTagOrigins,
|
|
55
|
+
sourceLocationOf,
|
|
56
|
+
type CompositeCallSite,
|
|
55
57
|
type CompositeParamScope,
|
|
58
|
+
type HostBindings,
|
|
56
59
|
} from "./param-deps";
|
|
57
|
-
import { getInterpolationFields, setPathProvenance } from "../provenance";
|
|
60
|
+
import { getInterpolationFields, getProvenance, setPathProvenance, setProvenance } from "../provenance";
|
|
61
|
+
import { foldProvenanceOfEntities, type FoldProvenance } from "../fold-provenance";
|
|
62
|
+
import { collectEntities } from "./collect";
|
|
58
63
|
import { intrinsicCallFoldsEagerly, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
|
|
59
64
|
import type { BuildParamValue } from "../build-params";
|
|
60
65
|
|
|
@@ -1376,6 +1381,13 @@ interface ResolveCtx {
|
|
|
1376
1381
|
* failure as before #1020 — this is strictly additive.
|
|
1377
1382
|
*/
|
|
1378
1383
|
externals: Map<string, unknown>;
|
|
1384
|
+
/**
|
|
1385
|
+
* INTENTIUS/typescript-as-data#248 — the names in {@link externals} a
|
|
1386
|
+
* lexicon package's export supplied. A props expression that reads one is a
|
|
1387
|
+
* `host-value` origin rather than the file's own `direct` one. Absent on a
|
|
1388
|
+
* context that never records direct-entity provenance.
|
|
1389
|
+
*/
|
|
1390
|
+
hostValues?: ReadonlySet<string>;
|
|
1379
1391
|
/**
|
|
1380
1392
|
* chant #1020 — for a `file`-local import name whose cross-file
|
|
1381
1393
|
* resolution attempt failed, WHY (a located, human-readable reason —
|
|
@@ -1479,6 +1491,13 @@ interface CompositeParamRecorder {
|
|
|
1479
1491
|
* `fold()` sees them.
|
|
1480
1492
|
*/
|
|
1481
1493
|
consts: Map<string, ts.Expression>;
|
|
1494
|
+
/**
|
|
1495
|
+
* chant #3597 — the composite call this body was reached from, when the
|
|
1496
|
+
* fold saw it as written. A call made from already-folded values (the
|
|
1497
|
+
* nested-value path, {@link resolveCompositeCall}) has no node, and its
|
|
1498
|
+
* members record origins without locations.
|
|
1499
|
+
*/
|
|
1500
|
+
call?: CompositeCallSite;
|
|
1482
1501
|
}
|
|
1483
1502
|
|
|
1484
1503
|
/** `{ 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. */
|
|
@@ -1698,7 +1717,7 @@ async function resolveImportedCall(
|
|
|
1698
1717
|
|
|
1699
1718
|
if (factory) {
|
|
1700
1719
|
const args = await resolveCallArguments(node, calleeName, ctx);
|
|
1701
|
-
const interpreted = await interpretCompositeFactory(factory, args, ctx);
|
|
1720
|
+
const interpreted = await interpretCompositeFactory(factory, args, ctx, { file: ctx.file, node });
|
|
1702
1721
|
if (interpreted) return interpreted.value;
|
|
1703
1722
|
// Declined — the body's shape passed but something in it did not resolve
|
|
1704
1723
|
// (an identifier the module's own imports don't reach, a constructor
|
|
@@ -1808,7 +1827,17 @@ async function invokeResolvedCallee(
|
|
|
1808
1827
|
const packageInternal = ctx.packageRoot !== undefined && isUnderDirectory(modulePath, ctx.packageRoot);
|
|
1809
1828
|
if (isProjectImport(binding.specifier, ctx.file) && !packageInternal) executionCounts.projectFactoryInvocations += 1;
|
|
1810
1829
|
|
|
1811
|
-
|
|
1830
|
+
const result = (Fn as (...fnArgs: unknown[]) => unknown)(...args);
|
|
1831
|
+
// chant#3608 — a host composite invoked rather than interpreted (one the
|
|
1832
|
+
// host publishes, or one whose body interpretation declined) returns the
|
|
1833
|
+
// host's own instance. Mark it as the interpreted form is marked, so an
|
|
1834
|
+
// instance exported whole is expanded and its members, whole or
|
|
1835
|
+
// destructured, are known to come from a composite.
|
|
1836
|
+
const compositeName = (Fn as { compositeName?: unknown }).compositeName;
|
|
1837
|
+
if (typeof compositeName === "string" && isIndexableObject(result) && !isCompositeInstance(result)) {
|
|
1838
|
+
markHostCompositeInstance(result, compositeName);
|
|
1839
|
+
}
|
|
1840
|
+
return result;
|
|
1812
1841
|
}
|
|
1813
1842
|
|
|
1814
1843
|
/**
|
|
@@ -1902,16 +1931,22 @@ async function resolveCallArguments(
|
|
|
1902
1931
|
const args: unknown[] = [];
|
|
1903
1932
|
for (const argNode of node.arguments) {
|
|
1904
1933
|
const live = await resolveLiveValue(argNode, ctx);
|
|
1905
|
-
if (live
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
//
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1934
|
+
if (live !== undefined) {
|
|
1935
|
+
args.push(live.value);
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
// chant#3610 — the bindings hold for this argument only, so a read of the
|
|
1939
|
+
// same name outside any argument still refuses, whatever the declarator order.
|
|
1940
|
+
const bound = await resolveSameFileCallReferences(argNode, ctx);
|
|
1941
|
+
try {
|
|
1942
|
+
// chant #1039 — a folded (non-live) argument may itself contain a
|
|
1943
|
+
// registered intrinsic tagged template; revive it into the real value
|
|
1944
|
+
// before the composite factory actually runs on it (see the "Intrinsic
|
|
1945
|
+
// revival" section below `resolveResourceEntity` uses the same way).
|
|
1946
|
+
args.push(await reviveFoldedValue(fold(argNode, ctx.consts, ctx.intrinsics, ctx.externals), ctx, helperArgs));
|
|
1947
|
+
} finally {
|
|
1948
|
+
for (const name of bound) ctx.externals.delete(name);
|
|
1949
|
+
}
|
|
1915
1950
|
}
|
|
1916
1951
|
return args;
|
|
1917
1952
|
}
|
|
@@ -1933,8 +1968,12 @@ async function resolveCallArguments(
|
|
|
1933
1968
|
* resolution that fails, or gives anything but a Declarable or a composite
|
|
1934
1969
|
* instance, is left alone too, so the reference rejects with the message it
|
|
1935
1970
|
* always had. Inside a factory body `locals` is empty, so nothing here applies.
|
|
1971
|
+
*
|
|
1972
|
+
* chant#3610 — returns the names it bound, which the caller removes from
|
|
1973
|
+
* `externals` once the argument has folded. A host composite's instance
|
|
1974
|
+
* counts as a composite instance here, as it does everywhere else.
|
|
1936
1975
|
*/
|
|
1937
|
-
async function resolveSameFileCallReferences(node: ts.Node, ctx: ResolveCtx): Promise<
|
|
1976
|
+
async function resolveSameFileCallReferences(node: ts.Node, ctx: ResolveCtx): Promise<string[]> {
|
|
1938
1977
|
const names = new Set<string>();
|
|
1939
1978
|
const visit = (n: ts.Node): void => {
|
|
1940
1979
|
if (ts.isIdentifier(n)) {
|
|
@@ -1950,6 +1989,7 @@ async function resolveSameFileCallReferences(node: ts.Node, ctx: ResolveCtx): Pr
|
|
|
1950
1989
|
ts.forEachChild(n, visit);
|
|
1951
1990
|
};
|
|
1952
1991
|
visit(node);
|
|
1992
|
+
const bound: string[] = [];
|
|
1953
1993
|
for (const name of names) {
|
|
1954
1994
|
let resolved: LiveResolution;
|
|
1955
1995
|
try {
|
|
@@ -1957,10 +1997,17 @@ async function resolveSameFileCallReferences(node: ts.Node, ctx: ResolveCtx): Pr
|
|
|
1957
1997
|
} catch {
|
|
1958
1998
|
continue;
|
|
1959
1999
|
}
|
|
1960
|
-
if (
|
|
2000
|
+
if (
|
|
2001
|
+
resolved &&
|
|
2002
|
+
(isDeclarable(resolved.value) ||
|
|
2003
|
+
isCompositeInstance(resolved.value) ||
|
|
2004
|
+
hostCompositeName(resolved.value) !== undefined)
|
|
2005
|
+
) {
|
|
1961
2006
|
ctx.externals.set(name, resolved.value);
|
|
2007
|
+
bound.push(name);
|
|
1962
2008
|
}
|
|
1963
2009
|
}
|
|
2010
|
+
return bound;
|
|
1964
2011
|
}
|
|
1965
2012
|
|
|
1966
2013
|
/** A same-file `const name = Callee(...)` that `fold()` would refuse as a function call used as a value. */
|
|
@@ -2798,6 +2845,7 @@ function isLiteralPropertyNameNode(node: ts.PropertyName): boolean {
|
|
|
2798
2845
|
function compositeParamRecorder(
|
|
2799
2846
|
factory: InterpretableFactory,
|
|
2800
2847
|
moduleConsts: ReadonlyMap<string, ts.Expression>,
|
|
2848
|
+
call?: CompositeCallSite,
|
|
2801
2849
|
): CompositeParamRecorder {
|
|
2802
2850
|
const whole = new Set<string>();
|
|
2803
2851
|
const destructured = new Map<string, string>();
|
|
@@ -2828,7 +2876,7 @@ function compositeParamRecorder(
|
|
|
2828
2876
|
}
|
|
2829
2877
|
}
|
|
2830
2878
|
|
|
2831
|
-
return { composite: factory.compositeName, scope: { whole, destructured }, consts };
|
|
2879
|
+
return { composite: factory.compositeName, scope: { whole, destructured }, consts, ...(call ? { call } : {}) };
|
|
2832
2880
|
}
|
|
2833
2881
|
|
|
2834
2882
|
/**
|
|
@@ -2852,10 +2900,21 @@ function stampCompositeOrigins(entity: unknown, node: ts.NewExpression, ctx: Res
|
|
|
2852
2900
|
}
|
|
2853
2901
|
}
|
|
2854
2902
|
if (!propsArg) return;
|
|
2855
|
-
|
|
2903
|
+
stampCompositeCall(entity, recorder);
|
|
2904
|
+
const origins = collectCompositeOrigins(propsArg, recorder.consts, recorder.scope, recorder.composite, recorder.call);
|
|
2856
2905
|
for (const [path, origin] of Object.entries(origins)) setPathProvenance(entity, path, origin);
|
|
2857
2906
|
}
|
|
2858
2907
|
|
|
2908
|
+
/**
|
|
2909
|
+
* chant #3597 — record where the composite call that expanded `entity` was
|
|
2910
|
+
* written, beside the per-path origins and in the same pass. First writer
|
|
2911
|
+
* wins, as for every provenance field, so the innermost call is kept.
|
|
2912
|
+
*/
|
|
2913
|
+
function stampCompositeCall(entity: object, recorder: CompositeParamRecorder): void {
|
|
2914
|
+
if (!recorder.call) return;
|
|
2915
|
+
setProvenance(entity, { compositeCall: sourceLocationOf(recorder.call.file, recorder.call.node) });
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2859
2918
|
/**
|
|
2860
2919
|
* chant #3212 — the same record for an entity a registered tag built inside an
|
|
2861
2920
|
* interpreted factory body (`` table`CREATE TABLE ${props.name} ...` ``).
|
|
@@ -2873,7 +2932,16 @@ function stampTagOrigins(entity: unknown, node: ts.TaggedTemplateExpression, ctx
|
|
|
2873
2932
|
const interpolations = ts.isTemplateExpression(node.template)
|
|
2874
2933
|
? node.template.templateSpans.map((span) => span.expression)
|
|
2875
2934
|
: [];
|
|
2876
|
-
|
|
2935
|
+
stampCompositeCall(entity, recorder);
|
|
2936
|
+
const origins = collectTagOrigins(
|
|
2937
|
+
interpolations,
|
|
2938
|
+
fields,
|
|
2939
|
+
entity.props,
|
|
2940
|
+
recorder.consts,
|
|
2941
|
+
recorder.scope,
|
|
2942
|
+
recorder.composite,
|
|
2943
|
+
recorder.call,
|
|
2944
|
+
);
|
|
2877
2945
|
for (const [path, origin] of Object.entries(origins)) setPathProvenance(entity, path, origin);
|
|
2878
2946
|
}
|
|
2879
2947
|
|
|
@@ -2892,6 +2960,7 @@ async function interpretCompositeFactory(
|
|
|
2892
2960
|
factory: InterpretableFactory,
|
|
2893
2961
|
args: readonly unknown[],
|
|
2894
2962
|
ctx: ResolveCtx,
|
|
2963
|
+
call?: CompositeCallSite,
|
|
2895
2964
|
): Promise<{ value: unknown } | undefined> {
|
|
2896
2965
|
// A `Composite()` factory takes exactly one props argument. Anything else at
|
|
2897
2966
|
// the call site means the callee is being used as something this doesn't
|
|
@@ -2932,7 +3001,7 @@ async function interpretCompositeFactory(
|
|
|
2932
3001
|
interpretDepth: ctx.interpretDepth + 1,
|
|
2933
3002
|
// chant #2161 — set unconditionally, and NOT inherited from `ctx`: a nested
|
|
2934
3003
|
// composite records its own parameters, never the enclosing one's.
|
|
2935
|
-
compositeParams: compositeParamRecorder(factory, scope.consts),
|
|
3004
|
+
compositeParams: compositeParamRecorder(factory, scope.consts, call),
|
|
2936
3005
|
};
|
|
2937
3006
|
|
|
2938
3007
|
const bind = (name: string, value: unknown): void => {
|
|
@@ -2973,7 +3042,7 @@ async function interpretCompositeFactory(
|
|
|
2973
3042
|
// rejecting it for not being chant's rather than for being malformed.
|
|
2974
3043
|
// Chant's own form still goes through `Composite`, unchanged.
|
|
2975
3044
|
const value = factory.hostForm
|
|
2976
|
-
? members
|
|
3045
|
+
? markHostCompositeInstance(members, factory.compositeName)
|
|
2977
3046
|
: (Composite<void, CompositeMembers>(() => members as CompositeMembers, factory.compositeName))();
|
|
2978
3047
|
executionCounts.factoryInterpretations += 1;
|
|
2979
3048
|
return { value };
|
|
@@ -2987,6 +3056,53 @@ async function interpretCompositeFactory(
|
|
|
2987
3056
|
}
|
|
2988
3057
|
}
|
|
2989
3058
|
|
|
3059
|
+
/**
|
|
3060
|
+
* chant#3598 — a host composite instance's own marker. Non-enumerable and
|
|
3061
|
+
* symbol-keyed, like provenance, so nothing that serializes or compares the
|
|
3062
|
+
* exports sees it. It carries the composite's name, which the members object
|
|
3063
|
+
* otherwise has no record of once it is returned.
|
|
3064
|
+
*/
|
|
3065
|
+
const HOST_COMPOSITE_INSTANCE = Symbol.for("chant.fold.hostCompositeInstance");
|
|
3066
|
+
|
|
3067
|
+
/**
|
|
3068
|
+
* Mark `members` as the instance a host's composite produced, and return it
|
|
3069
|
+
* unchanged.
|
|
3070
|
+
*
|
|
3071
|
+
* chant#3608 — every declarable member, and every member of a nested host
|
|
3072
|
+
* instance, is also stamped with the composite, the way `expandComposite`
|
|
3073
|
+
* stamps a chant instance's members. A destructured member is never expanded,
|
|
3074
|
+
* so this is the only record it has that a composite wrote it, and without
|
|
3075
|
+
* it a path the fold did not attribute reads as authored and so `direct`.
|
|
3076
|
+
* `setProvenance` keeps the first writer, and a nested instance was marked
|
|
3077
|
+
* when its own call returned, so a member keeps its innermost composite.
|
|
3078
|
+
*/
|
|
3079
|
+
function markHostCompositeInstance(members: Record<string, unknown>, compositeName: string): Record<string, unknown> {
|
|
3080
|
+
if (Object.isExtensible(members) && !Object.prototype.hasOwnProperty.call(members, HOST_COMPOSITE_INSTANCE)) {
|
|
3081
|
+
Object.defineProperty(members, HOST_COMPOSITE_INSTANCE, {
|
|
3082
|
+
value: compositeName,
|
|
3083
|
+
enumerable: false,
|
|
3084
|
+
configurable: true,
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
stampHostCompositeMembers(members, compositeName);
|
|
3088
|
+
return members;
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
/** Stamp `composite` on each declarable member of a host instance, descending into nested host instances. */
|
|
3092
|
+
function stampHostCompositeMembers(members: Record<string, unknown>, compositeName: string): void {
|
|
3093
|
+
for (const member of Object.values(members)) {
|
|
3094
|
+
if (isDeclarable(member)) setProvenance(member, { composite: compositeName });
|
|
3095
|
+
else if (hostCompositeName(member) !== undefined) stampHostCompositeMembers(member as Record<string, unknown>, compositeName);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
/** The composite name a host composite instance was marked with, or `undefined` for any other value. */
|
|
3100
|
+
function hostCompositeName(value: unknown): string | undefined {
|
|
3101
|
+
if (!isIndexableObject(value)) return undefined;
|
|
3102
|
+
const name = (value as Record<symbol, unknown>)[HOST_COMPOSITE_INSTANCE];
|
|
3103
|
+
return typeof name === "string" ? name : undefined;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
2990
3106
|
/** Evaluate an admissible factory body's statements in order, binding each `const`, and return the `return` expression's value. */
|
|
2991
3107
|
async function interpretFactoryBody(
|
|
2992
3108
|
fn: ts.ArrowFunction | ts.FunctionExpression,
|
|
@@ -3829,13 +3945,19 @@ function paramLocalNames(ctx: ResolveCtx): Set<string> {
|
|
|
3829
3945
|
/**
|
|
3830
3946
|
* chant #1443 — record which build parameters each of a resource's authored
|
|
3831
3947
|
* property expressions reads, before fold substitutes them away. Best-effort
|
|
3832
|
-
* and additive: a
|
|
3833
|
-
*
|
|
3948
|
+
* and additive: a constructor called with no object literal records nothing.
|
|
3949
|
+
*
|
|
3950
|
+
* INTENTIUS/typescript-as-data#248 — and which of them read host code, a
|
|
3951
|
+
* method call, an eager lexicon helper or a lexicon export, so fold provenance
|
|
3952
|
+
* reports those `unknown` rather than `direct`.
|
|
3834
3953
|
*/
|
|
3835
3954
|
function stampParamDependencies(entity: unknown, node: ts.NewExpression, ctx: ResolveCtx): void {
|
|
3836
3955
|
if (typeof entity !== "object" || entity === null) return;
|
|
3837
3956
|
const paramLocals = paramLocalNames(ctx);
|
|
3838
|
-
|
|
3957
|
+
const host: HostBindings = {
|
|
3958
|
+
values: ctx.hostValues ?? new Set(),
|
|
3959
|
+
eagerCalls: new Set(ctx.intrinsics.filter((i) => intrinsicCallFoldsEagerly(i)).map((i) => i.name)),
|
|
3960
|
+
};
|
|
3839
3961
|
// The same argument `foldResource` treats as props: the first object literal.
|
|
3840
3962
|
let propsArg: ts.ObjectLiteralExpression | undefined;
|
|
3841
3963
|
for (const argument of node.arguments ?? []) {
|
|
@@ -3845,7 +3967,7 @@ function stampParamDependencies(entity: unknown, node: ts.NewExpression, ctx: Re
|
|
|
3845
3967
|
}
|
|
3846
3968
|
}
|
|
3847
3969
|
if (!propsArg) return;
|
|
3848
|
-
for (const [path, origin] of Object.entries(collectParamDependencies(propsArg, ctx.consts, paramLocals))) {
|
|
3970
|
+
for (const [path, origin] of Object.entries(collectParamDependencies(propsArg, ctx.consts, paramLocals, host))) {
|
|
3849
3971
|
setPathProvenance(entity, path, origin);
|
|
3850
3972
|
}
|
|
3851
3973
|
}
|
|
@@ -4085,13 +4207,20 @@ async function buildExternals(
|
|
|
4085
4207
|
* a path lexicon's are, never folded as project files.
|
|
4086
4208
|
*/
|
|
4087
4209
|
extraLexiconRoots: readonly string[] = [],
|
|
4088
|
-
): Promise<{
|
|
4210
|
+
): Promise<{
|
|
4211
|
+
externals: Map<string, unknown>;
|
|
4212
|
+
failures: Map<string, string>;
|
|
4213
|
+
liveSources: Set<string>;
|
|
4214
|
+
/** Local names bound to a lexicon package's export (INTENTIUS/typescript-as-data#248). */
|
|
4215
|
+
hostValues: Set<string>;
|
|
4216
|
+
}> {
|
|
4089
4217
|
const lexiconRoots =
|
|
4090
4218
|
extraLexiconRoots.length === 0 ? session.lexiconRoots : [...session.lexiconRoots, ...extraLexiconRoots];
|
|
4091
4219
|
const externals = new Map<string, unknown>();
|
|
4092
4220
|
const failures = new Map<string, string>();
|
|
4093
4221
|
// chant #1044 — see `FoldFileResult.liveSources`.
|
|
4094
4222
|
const liveSources = new Set<string>();
|
|
4223
|
+
const hostValues = new Set<string>();
|
|
4095
4224
|
|
|
4096
4225
|
for (const [localName, binding] of imports) {
|
|
4097
4226
|
// chant #1064 — a named `params` import that resolves to chant-core's own
|
|
@@ -4168,6 +4297,7 @@ async function buildExternals(
|
|
|
4168
4297
|
const lexiconExport = await resolveActiveLexiconExport(binding, file, session);
|
|
4169
4298
|
if (lexiconExport) {
|
|
4170
4299
|
externals.set(localName, lexiconExport.value);
|
|
4300
|
+
hostValues.add(localName);
|
|
4171
4301
|
continue;
|
|
4172
4302
|
}
|
|
4173
4303
|
// Every other bare specifier (a non-lexicon vendor package, a lexicon
|
|
@@ -4202,7 +4332,10 @@ async function buildExternals(
|
|
|
4202
4332
|
// with no `liveSources` edge for the same reason.
|
|
4203
4333
|
if (session.lexiconModules.has(targetPath)) {
|
|
4204
4334
|
const lexiconExport = await resolveActiveLexiconExport(binding, file, session);
|
|
4205
|
-
if (lexiconExport)
|
|
4335
|
+
if (lexiconExport) {
|
|
4336
|
+
externals.set(localName, lexiconExport.value);
|
|
4337
|
+
hostValues.add(localName);
|
|
4338
|
+
}
|
|
4206
4339
|
continue;
|
|
4207
4340
|
}
|
|
4208
4341
|
// chant#2590 — another file of a multi-file path lexicon is a lexicon
|
|
@@ -4220,7 +4353,10 @@ async function buildExternals(
|
|
|
4220
4353
|
// constructor or composite resolves through the import path.
|
|
4221
4354
|
if (inPathLexiconRoot(targetPath, extraLexiconRoots)) {
|
|
4222
4355
|
const value = await packageDataExport(targetPath, binding.imported, session);
|
|
4223
|
-
if (value)
|
|
4356
|
+
if (value) {
|
|
4357
|
+
externals.set(localName, value.value);
|
|
4358
|
+
hostValues.add(localName);
|
|
4359
|
+
}
|
|
4224
4360
|
continue;
|
|
4225
4361
|
}
|
|
4226
4362
|
if (inPathLexiconRoot(targetPath, lexiconRoots)) continue;
|
|
@@ -4269,7 +4405,7 @@ async function buildExternals(
|
|
|
4269
4405
|
}
|
|
4270
4406
|
}
|
|
4271
4407
|
|
|
4272
|
-
return { externals, failures, liveSources };
|
|
4408
|
+
return { externals, failures, liveSources, hostValues };
|
|
4273
4409
|
}
|
|
4274
4410
|
|
|
4275
4411
|
/**
|
|
@@ -4345,7 +4481,12 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
4345
4481
|
if (scan.declarators.length === 0) return { ok: false, reason: "no foldable resource exports" };
|
|
4346
4482
|
|
|
4347
4483
|
const collected = collectImports(sourceFile);
|
|
4348
|
-
const { externals, failures, liveSources } = await buildExternals(
|
|
4484
|
+
const { externals, failures, liveSources, hostValues } = await buildExternals(
|
|
4485
|
+
file,
|
|
4486
|
+
collected.named,
|
|
4487
|
+
collected.namespaces,
|
|
4488
|
+
session,
|
|
4489
|
+
);
|
|
4349
4490
|
|
|
4350
4491
|
const ctx: ResolveCtx = {
|
|
4351
4492
|
file,
|
|
@@ -4356,6 +4497,7 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
4356
4497
|
memo: new Map(),
|
|
4357
4498
|
intrinsics: session.intrinsics,
|
|
4358
4499
|
externals,
|
|
4500
|
+
hostValues,
|
|
4359
4501
|
crossFileFailures: failures,
|
|
4360
4502
|
importCache: session.importCache,
|
|
4361
4503
|
resolvePathCache: session.resolvePathCache,
|
|
@@ -4850,6 +4992,20 @@ export interface FoldProjectVerdict {
|
|
|
4850
4992
|
readonly taintedBy?: { from: string; kind: TaintEdgeKind };
|
|
4851
4993
|
/** The file's complete export namespace, present only when `verdict` is "fold". */
|
|
4852
4994
|
readonly exports?: ReadonlyMap<string, unknown>;
|
|
4995
|
+
/**
|
|
4996
|
+
* chant#3598 — fold provenance for the entities this file contributes,
|
|
4997
|
+
* keyed by entity name, in the shape `build()` reports as
|
|
4998
|
+
* `BuildResult.foldProvenance`. Present only when `verdict` is "fold" and
|
|
4999
|
+
* the folded files' entities could be collected (a duplicate entity name
|
|
5000
|
+
* fails collection for the whole set, as it fails a build, and then no
|
|
5001
|
+
* verdict carries it). A file that folds and declares nothing with
|
|
5002
|
+
* properties carries `{}`.
|
|
5003
|
+
*
|
|
5004
|
+
* A "run" file has no entry: foldProject never executes one, so it has no
|
|
5005
|
+
* entities to attribute. That holds under `sandbox` too, where nothing
|
|
5006
|
+
* crosses the child-process wire because no child is started.
|
|
5007
|
+
*/
|
|
5008
|
+
readonly foldProvenance?: FoldProvenance;
|
|
4853
5009
|
}
|
|
4854
5010
|
|
|
4855
5011
|
/**
|
|
@@ -4910,12 +5066,29 @@ export async function foldProject(
|
|
|
4910
5066
|
})),
|
|
4911
5067
|
);
|
|
4912
5068
|
|
|
5069
|
+
const folded = files.filter((file) => attempts.get(file)?.ok === true && !plan.tainted.has(file));
|
|
5070
|
+
const provenanceByFile = foldProvenanceByFile(
|
|
5071
|
+
folded.map((file) => {
|
|
5072
|
+
const attempt = attempts.get(file);
|
|
5073
|
+
return {
|
|
5074
|
+
file,
|
|
5075
|
+
exports: attempt?.ok === true ? expandHostCompositeExports(Object.fromEntries(attempt.exportedValues)) : {},
|
|
5076
|
+
};
|
|
5077
|
+
}),
|
|
5078
|
+
);
|
|
5079
|
+
|
|
4913
5080
|
const out = new Map<string, FoldProjectVerdict>();
|
|
4914
5081
|
for (const file of files) {
|
|
4915
5082
|
const attempt = attempts.get(file)!;
|
|
4916
5083
|
const tentative = attempt.ok ? "fold" : "run";
|
|
4917
5084
|
if (attempt.ok && !plan.tainted.has(file)) {
|
|
4918
|
-
|
|
5085
|
+
const foldProvenance = provenanceByFile?.get(file);
|
|
5086
|
+
out.set(file, {
|
|
5087
|
+
verdict: "fold",
|
|
5088
|
+
tentative,
|
|
5089
|
+
exports: attempt.exportedValues,
|
|
5090
|
+
...(foldProvenance ? { foldProvenance } : {}),
|
|
5091
|
+
});
|
|
4919
5092
|
continue;
|
|
4920
5093
|
}
|
|
4921
5094
|
out.set(file, {
|
|
@@ -4927,3 +5100,124 @@ export async function foldProject(
|
|
|
4927
5100
|
}
|
|
4928
5101
|
return out;
|
|
4929
5102
|
}
|
|
5103
|
+
|
|
5104
|
+
/**
|
|
5105
|
+
* chant#3598 — the folded files' fold provenance, split by declaring file.
|
|
5106
|
+
*
|
|
5107
|
+
* This is `build()`'s computation, not a second one: the same
|
|
5108
|
+
* `collectEntities` over the same folded export namespaces `discover()` hands
|
|
5109
|
+
* it, then `foldProvenanceOfEntities` with `getProvenance`. One call over the
|
|
5110
|
+
* whole folded set rather than one per file, because composite expansion and
|
|
5111
|
+
* cross-directory name disambiguation (#932) are decided over the set and an
|
|
5112
|
+
* entity's name is part of what is being reported. The build root is the
|
|
5113
|
+
* files' common directory, which is what `chant build <dir>` passes when every
|
|
5114
|
+
* file sits under `<dir>`.
|
|
5115
|
+
*
|
|
5116
|
+
* Returns `undefined` when collection throws (a duplicate name), the case
|
|
5117
|
+
* where a build has no entities either.
|
|
5118
|
+
*/
|
|
5119
|
+
function foldProvenanceByFile(
|
|
5120
|
+
modules: Array<{ file: string; exports: Record<string, unknown> }>,
|
|
5121
|
+
): Map<string, FoldProvenance> | undefined {
|
|
5122
|
+
if (modules.length === 0) return new Map();
|
|
5123
|
+
let entities: Map<string, Declarable>;
|
|
5124
|
+
try {
|
|
5125
|
+
entities = collectEntities(modules, commonDirectory(modules.map((m) => m.file)));
|
|
5126
|
+
} catch {
|
|
5127
|
+
return undefined;
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
const byResolved = new Map<string, FoldProvenance>();
|
|
5131
|
+
const out = new Map<string, FoldProvenance>();
|
|
5132
|
+
for (const { file } of modules) {
|
|
5133
|
+
const record: FoldProvenance = {};
|
|
5134
|
+
byResolved.set(resolvePath(file), record);
|
|
5135
|
+
out.set(file, record);
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5138
|
+
const all = foldProvenanceOfEntities(entities, getProvenance);
|
|
5139
|
+
for (const [name, record] of Object.entries(all)) {
|
|
5140
|
+
const sourceFile = getProvenance(entities.get(name) as Declarable)?.sourceFile;
|
|
5141
|
+
const target = sourceFile === undefined ? undefined : byResolved.get(resolvePath(sourceFile));
|
|
5142
|
+
if (target) target[name] = record;
|
|
5143
|
+
}
|
|
5144
|
+
return out;
|
|
5145
|
+
}
|
|
5146
|
+
|
|
5147
|
+
/**
|
|
5148
|
+
* chant#3598 — a file's exports with every host composite instance replaced by
|
|
5149
|
+
* its members, for {@link foldProvenanceByFile} only.
|
|
5150
|
+
*
|
|
5151
|
+
* A host's registration form (chant#2442) returns the members object the
|
|
5152
|
+
* factory built, not a chant `CompositeInstance`, because wrapping it would run
|
|
5153
|
+
* chant's member validation against the host's own entities. `collectEntities`
|
|
5154
|
+
* expands only chant's instances, so an instance exported whole
|
|
5155
|
+
* (`export const store = Store({...})`) contributed no entity and therefore no
|
|
5156
|
+
* record, while a destructured member, a plain declarable export, did.
|
|
5157
|
+
*
|
|
5158
|
+
* The members are named and stamped the way `expandComposite` names and stamps
|
|
5159
|
+
* a chant instance: `${exportName}${Member}`, with a whole-entity `composite`
|
|
5160
|
+
* origin under the paths the fold already attributed. That origin is written
|
|
5161
|
+
* before `collectEntities` adds its `authored` one, and the first writer wins,
|
|
5162
|
+
* so a path the fold did not attribute is `unknown` rather than `direct`.
|
|
5163
|
+
*
|
|
5164
|
+
* Only a top-level export is expanded. A host instance nested in another
|
|
5165
|
+
* composite's body or placed in an array does not fold today, so its file is
|
|
5166
|
+
* `run` and has no record to give.
|
|
5167
|
+
*
|
|
5168
|
+
* The exports a caller receives in the verdict are the originals.
|
|
5169
|
+
*/
|
|
5170
|
+
function expandHostCompositeExports(exports: Record<string, unknown>): Record<string, unknown> {
|
|
5171
|
+
const out: Record<string, unknown> = {};
|
|
5172
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
5173
|
+
if (hostCompositeName(value) === undefined) {
|
|
5174
|
+
out[name] = value;
|
|
5175
|
+
continue;
|
|
5176
|
+
}
|
|
5177
|
+
expandHostCompositeInstance(name, value as Record<string, unknown>, name, out);
|
|
5178
|
+
}
|
|
5179
|
+
return out;
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
/**
|
|
5183
|
+
* chant#3608 — one host instance's members into `out`, a nested host instance
|
|
5184
|
+
* expanded in place under its member's name (`siteMainBucket`), as
|
|
5185
|
+
* `expandComposite` expands a nested chant instance. Every member belongs to
|
|
5186
|
+
* the top-level `instance`; its composite is the innermost one, which
|
|
5187
|
+
* {@link markHostCompositeInstance} already stamped, so the whole-entity
|
|
5188
|
+
* origin names that composite too.
|
|
5189
|
+
*/
|
|
5190
|
+
function expandHostCompositeInstance(
|
|
5191
|
+
prefix: string,
|
|
5192
|
+
members: Record<string, unknown>,
|
|
5193
|
+
instance: string,
|
|
5194
|
+
out: Record<string, unknown>,
|
|
5195
|
+
): void {
|
|
5196
|
+
const compositeName = hostCompositeName(members) as string;
|
|
5197
|
+
for (const [memberName, member] of Object.entries(members)) {
|
|
5198
|
+
if (memberName.length === 0) continue;
|
|
5199
|
+
const fullName = `${prefix}${memberName[0].toUpperCase()}${memberName.slice(1)}`;
|
|
5200
|
+
if (hostCompositeName(member) !== undefined) {
|
|
5201
|
+
expandHostCompositeInstance(fullName, member as Record<string, unknown>, instance, out);
|
|
5202
|
+
continue;
|
|
5203
|
+
}
|
|
5204
|
+
if (!isDeclarable(member)) continue;
|
|
5205
|
+
setProvenance(member, { composite: compositeName, compositeInstance: instance });
|
|
5206
|
+
const composite = getProvenance(member)?.composite ?? compositeName;
|
|
5207
|
+
setPathProvenance(member, "", { kind: "composite", composite, instance });
|
|
5208
|
+
out[fullName] = member;
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
/** The deepest directory every one of `files` sits under. */
|
|
5213
|
+
function commonDirectory(files: readonly string[]): string {
|
|
5214
|
+
const split = files.map((file) => dirname(resolvePath(file)).split(sep));
|
|
5215
|
+
const first = split[0];
|
|
5216
|
+
let depth = first.length;
|
|
5217
|
+
for (const parts of split.slice(1)) {
|
|
5218
|
+
let i = 0;
|
|
5219
|
+
while (i < depth && i < parts.length && parts[i] === first[i]) i++;
|
|
5220
|
+
depth = i;
|
|
5221
|
+
}
|
|
5222
|
+
return first.slice(0, depth).join(sep) || sep;
|
|
5223
|
+
}
|
|
@@ -96,15 +96,28 @@ describe("lexicon-package and re-exported composites are interpreted (chant#3247
|
|
|
96
96
|
expect(folded.errors).toEqual([]);
|
|
97
97
|
expect(foldExecutionCounts()).toMatchObject({ factoryInterpretations: 1, factoryInvocations: 0 });
|
|
98
98
|
const fields = folded.foldProvenance.sinkAssignmentActivityLogSink!.fields;
|
|
99
|
+
// chant#3597 — where the call and each argument were written in main.ts.
|
|
100
|
+
const file = join(srcDir, "main.ts");
|
|
101
|
+
const lines = CALL_SINK.split("\n");
|
|
102
|
+
const line = lines.findIndex((l) => l.includes("ActivityLogSink({")) + 1;
|
|
103
|
+
const at = (needle: string) => ({ file, line, column: lines[line - 1].indexOf(needle) + 1 });
|
|
104
|
+
const call = at("ActivityLogSink({");
|
|
105
|
+
const written: Record<string, string> = {
|
|
106
|
+
location: 'location: "eastus"',
|
|
107
|
+
workspaceId: 'workspaceId: "/subscriptions/s/workspaces/logs"',
|
|
108
|
+
};
|
|
99
109
|
const param = (...parameters: string[]) => ({
|
|
100
110
|
kind: "composite-parameter",
|
|
101
111
|
composite: "ActivityLogSink",
|
|
102
112
|
instance: "sink",
|
|
103
113
|
parameters,
|
|
114
|
+
call,
|
|
115
|
+
arguments: parameters.map((parameter) => ({ parameter, ...at(written[parameter]), text: written[parameter] })),
|
|
104
116
|
});
|
|
105
117
|
expect(fields.location).toEqual(param("location"));
|
|
106
118
|
expect(fields["parameters.logAnalytics.value"]).toEqual(param("workspaceId"));
|
|
107
|
-
expect(fields.name).toEqual({ kind: "composite-literal", composite: "ActivityLogSink", instance: "sink" });
|
|
119
|
+
expect(fields.name).toEqual({ kind: "composite-literal", composite: "ActivityLogSink", instance: "sink", call });
|
|
120
|
+
expect(folded.foldProvenance.sinkAssignmentActivityLogSink!.compositeCall).toEqual(call);
|
|
108
121
|
|
|
109
122
|
// Interpreting is not a second evaluation: the run path writes the same bytes.
|
|
110
123
|
const run = await build(srcDir, [propsSerializer("azure")], undefined, { fold: false, lexicons: ["azure"] });
|