@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
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import { describe, expect, test } from "vitest";
|
|
3
3
|
import { collectConsts } from "../fold/fold";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
argumentLocation,
|
|
6
|
+
collectCompositeOrigins,
|
|
7
|
+
collectParamDependencies,
|
|
8
|
+
collectTagOrigins,
|
|
9
|
+
type CompositeCallSite,
|
|
10
|
+
type HostBindings,
|
|
11
|
+
} from "./param-deps";
|
|
5
12
|
import type { PathOrigin } from "../provenance";
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* Collect dependencies for the props of the file's single
|
|
9
16
|
* `export const x = new Type({...})`, which is the shape every case here uses.
|
|
10
17
|
*/
|
|
11
|
-
function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOrigin> {
|
|
18
|
+
function depsOf(source: string, paramLocals = ["params"], host?: HostBindings): Record<string, PathOrigin> {
|
|
12
19
|
const file = ts.createSourceFile("fixture.ts", source, ts.ScriptTarget.Latest, true);
|
|
13
20
|
const consts = collectConsts(file);
|
|
14
21
|
let props: ts.ObjectLiteralExpression | undefined;
|
|
@@ -26,7 +33,7 @@ function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOr
|
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
if (!props) throw new Error("fixture has no `const x = new Type({...})`");
|
|
29
|
-
return collectParamDependencies(props, consts, new Set(paramLocals));
|
|
36
|
+
return collectParamDependencies(props, consts, new Set(paramLocals), host);
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
const param = (...names: string[]): PathOrigin => ({ kind: "build-param", params: names });
|
|
@@ -292,3 +299,156 @@ describe("collectTagOrigins", () => {
|
|
|
292
299
|
).toEqual({ "engine.args": fromParam("version"), engine: fixed });
|
|
293
300
|
});
|
|
294
301
|
});
|
|
302
|
+
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
304
|
+
// chant #3597 — where the argument behind a parameter was written.
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
/** The first call expression in `source`, as a call site in `/p/src/app.ts`. */
|
|
308
|
+
function callSiteOf(source: string): CompositeCallSite {
|
|
309
|
+
const file = ts.createSourceFile("/p/src/app.ts", source, ts.ScriptTarget.Latest, true);
|
|
310
|
+
let node: ts.CallExpression | undefined;
|
|
311
|
+
const visit = (n: ts.Node): void => {
|
|
312
|
+
if (!node && ts.isCallExpression(n)) node = n;
|
|
313
|
+
ts.forEachChild(n, visit);
|
|
314
|
+
};
|
|
315
|
+
visit(file);
|
|
316
|
+
if (!node) throw new Error("fixture has no call");
|
|
317
|
+
return { file: "/p/src/app.ts", node };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
describe("argumentLocation (#3597)", () => {
|
|
321
|
+
const source = [
|
|
322
|
+
'import { WebApp } from "./web-app";',
|
|
323
|
+
"",
|
|
324
|
+
"export const web = WebApp({",
|
|
325
|
+
' name: "web-app",',
|
|
326
|
+
" replicas: 3,",
|
|
327
|
+
' iam: { path: "/service/" },',
|
|
328
|
+
" port,",
|
|
329
|
+
"});",
|
|
330
|
+
].join("\n");
|
|
331
|
+
|
|
332
|
+
test("a top-level argument is the property as written, on its own line", () => {
|
|
333
|
+
expect(argumentLocation(callSiteOf(source), "replicas")).toEqual({
|
|
334
|
+
parameter: "replicas",
|
|
335
|
+
file: "/p/src/app.ts",
|
|
336
|
+
line: 5,
|
|
337
|
+
column: 3,
|
|
338
|
+
text: "replicas: 3",
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("a nested parameter path descends the argument's object literals", () => {
|
|
343
|
+
expect(argumentLocation(callSiteOf(source), "iam.path")).toEqual({
|
|
344
|
+
parameter: "iam.path",
|
|
345
|
+
file: "/p/src/app.ts",
|
|
346
|
+
line: 6,
|
|
347
|
+
column: 10,
|
|
348
|
+
text: 'path: "/service/"',
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("a shorthand property is written as its name", () => {
|
|
353
|
+
expect(argumentLocation(callSiteOf(source), "port")).toMatchObject({ line: 7, column: 3, text: "port" });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("a parameter the call leaves out falls back to the call, and claims no text", () => {
|
|
357
|
+
expect(argumentLocation(callSiteOf(source), "image")).toEqual({
|
|
358
|
+
parameter: "image",
|
|
359
|
+
file: "/p/src/app.ts",
|
|
360
|
+
line: 3,
|
|
361
|
+
column: 20,
|
|
362
|
+
});
|
|
363
|
+
expect(argumentLocation(callSiteOf(source), "iam.role")).not.toHaveProperty("text");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("an argument that is not an object literal falls back to the call", () => {
|
|
367
|
+
const location = argumentLocation(callSiteOf("export const web = WebApp(shared);"), "replicas");
|
|
368
|
+
expect(location).toEqual({ parameter: "replicas", file: "/p/src/app.ts", line: 1, column: 20 });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("collectCompositeOrigins records the locations in the same pass, in parameter order", () => {
|
|
372
|
+
const call = callSiteOf(source);
|
|
373
|
+
const body = ts.createSourceFile(
|
|
374
|
+
"factory.ts",
|
|
375
|
+
"const f = (props) => new Thing({ spec: { replicas: props.replicas }, label: `${props.name}-${props.iam.path}`, kind: \"web\" });",
|
|
376
|
+
ts.ScriptTarget.Latest,
|
|
377
|
+
true,
|
|
378
|
+
);
|
|
379
|
+
let props: ts.ObjectLiteralExpression | undefined;
|
|
380
|
+
const visit = (n: ts.Node): void => {
|
|
381
|
+
if (!props && ts.isNewExpression(n)) props = n.arguments?.find(ts.isObjectLiteralExpression);
|
|
382
|
+
ts.forEachChild(n, visit);
|
|
383
|
+
};
|
|
384
|
+
visit(body);
|
|
385
|
+
const origins = collectCompositeOrigins(
|
|
386
|
+
props as ts.ObjectLiteralExpression,
|
|
387
|
+
new Map(),
|
|
388
|
+
{ whole: new Set(["props"]), destructured: new Map() },
|
|
389
|
+
"WebApp",
|
|
390
|
+
call,
|
|
391
|
+
);
|
|
392
|
+
expect(origins["spec.replicas"]).toEqual({
|
|
393
|
+
kind: "composite-parameter",
|
|
394
|
+
composite: "WebApp",
|
|
395
|
+
parameters: ["replicas"],
|
|
396
|
+
arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 5, column: 3, text: "replicas: 3" }],
|
|
397
|
+
});
|
|
398
|
+
expect(origins.label).toMatchObject({
|
|
399
|
+
parameters: ["iam.path", "name"],
|
|
400
|
+
arguments: [
|
|
401
|
+
{ parameter: "iam.path", line: 6, text: 'path: "/service/"' },
|
|
402
|
+
{ parameter: "name", line: 4, text: 'name: "web-app"' },
|
|
403
|
+
],
|
|
404
|
+
});
|
|
405
|
+
// A literal carries no location: nothing at the call moves it.
|
|
406
|
+
expect(origins.kind).toEqual({ kind: "composite-literal", composite: "WebApp" });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("without a call site the origins are what they were", () => {
|
|
410
|
+
expect(compositeOriginsOf("return new Thing({ replicas: props.replicas });")).toEqual({
|
|
411
|
+
replicas: { kind: "composite-parameter", composite: "WebService", parameters: ["replicas"] },
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("collectParamDependencies with host bindings (typescript-as-data#248)", () => {
|
|
417
|
+
const host: HostBindings = { values: new Set(["AWS"]), eagerCalls: new Set(["matrix"]) };
|
|
418
|
+
const hostCall: PathOrigin = { kind: "host", reason: "host-call" };
|
|
419
|
+
const hostValue: PathOrigin = { kind: "host", reason: "host-value" };
|
|
420
|
+
|
|
421
|
+
test("a method call on a receiver is a host call, at its own path", () => {
|
|
422
|
+
const source = `export const x = new Thing({ name: "x".toUpperCase(), spec: { tag: [1, 2].join("-") }, fixed: "k" });`;
|
|
423
|
+
expect(depsOf(source, [], host)).toEqual({ name: hostCall, "spec.tag": hostCall });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("an eager lexicon helper is a host call, and a project function is not", () => {
|
|
427
|
+
const source = `export const x = new Thing({ a: matrix("os"), b: helper("os") });`;
|
|
428
|
+
expect(depsOf(source, [], host)).toEqual({ a: hostCall });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("a lexicon export is a host value, through a const and an operator", () => {
|
|
432
|
+
const source = [`const region = AWS.Region;`, "export const x = new Thing({ a: `${region}-x`, b: AWS });"].join("\n");
|
|
433
|
+
expect(depsOf(source, [], host)).toEqual({ a: hostValue, b: hostValue });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("a host read outranks a build parameter the same expression reads", () => {
|
|
437
|
+
const source = `export const x = new Thing({ name: params.name.toUpperCase(), size: params.size });`;
|
|
438
|
+
expect(depsOf(source, ["params"], host)).toEqual({ name: hostCall, size: param("size") });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("a sibling resource, a member name and a property key are not host reads", () => {
|
|
442
|
+
const source = [
|
|
443
|
+
`const logs = new Bucket({ name: "x".toUpperCase() });`,
|
|
444
|
+
`const AWS2 = 1;`,
|
|
445
|
+
`export const x = new Thing({ a: logs.Arn, b: { AWS: 1 }, c: thing.AWS });`,
|
|
446
|
+
].join("\n");
|
|
447
|
+
expect(depsOf(source, [], host)).toEqual({});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("a tag is not read, its interpolations are", () => {
|
|
451
|
+
const source = "export const x = new Thing({ a: Sub`${AWS.Region}`, b: Sub`plain` });";
|
|
452
|
+
expect(depsOf(source, [], { values: new Set(["AWS", "Sub"]), eagerCalls: new Set() })).toEqual({ a: hostValue });
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
|
-
import type { PathOrigin } from "../provenance";
|
|
2
|
+
import type { ArgumentLocation, HostOriginReason, PathOrigin, SourceLocation } from "../provenance";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Which build parameters an authored props expression READS, per path (chant
|
|
@@ -117,6 +117,94 @@ function readParams(
|
|
|
117
117
|
visit(expr);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* What a file binds to host code, for {@link hostReadOf} (INTENTIUS/typescript-as-data#248).
|
|
122
|
+
*/
|
|
123
|
+
export interface HostBindings {
|
|
124
|
+
/** Local names bound to a value a lexicon package exports. */
|
|
125
|
+
values: ReadonlySet<string>;
|
|
126
|
+
/** Local names of eager lexicon helpers: a call to one is evaluated by host code at fold time. */
|
|
127
|
+
eagerCalls: ReadonlySet<string>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The first host read in `expr`, following this file's own top-level consts,
|
|
132
|
+
* or `undefined` when it reads none (INTENTIUS/typescript-as-data#248).
|
|
133
|
+
*
|
|
134
|
+
* A method called on a receiver is `host-call` whatever the receiver is, since
|
|
135
|
+
* fold calls the real method on it; so is a call to an eager lexicon helper. An
|
|
136
|
+
* identifier bound to a lexicon export is `host-value`. A call fold reduces to
|
|
137
|
+
* an envelope (an authoring helper, a call-form intrinsic) or evaluates itself
|
|
138
|
+
* (a project function) is not host code, and only its arguments are read.
|
|
139
|
+
*
|
|
140
|
+
* Syntactic, like {@link readParams}, so it over-reports in one place: both
|
|
141
|
+
* arms of a conditional are read although only one is taken. That errs toward
|
|
142
|
+
* `unknown`, which never licenses an edit `direct` would not.
|
|
143
|
+
*/
|
|
144
|
+
export function hostReadOf(
|
|
145
|
+
expr: ts.Expression,
|
|
146
|
+
consts: ReadonlyMap<string, ts.Expression>,
|
|
147
|
+
paramLocals: ReadonlySet<string>,
|
|
148
|
+
host: HostBindings,
|
|
149
|
+
): HostOriginReason | undefined {
|
|
150
|
+
const followed = new Set<string>();
|
|
151
|
+
let found: HostOriginReason | undefined;
|
|
152
|
+
|
|
153
|
+
const visit = (node: ts.Node): void => {
|
|
154
|
+
if (found) return;
|
|
155
|
+
if (ts.isCallExpression(node)) {
|
|
156
|
+
const callee = unwrap(node.expression);
|
|
157
|
+
if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
|
|
158
|
+
found = "host-call";
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (ts.isIdentifier(callee) && !consts.has(callee.text) && host.eagerCalls.has(callee.text)) {
|
|
162
|
+
found = "host-call";
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const argument of node.arguments) visit(argument);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
170
|
+
// The member name is not a reference.
|
|
171
|
+
visit(node.expression);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ts.isIdentifier(node)) {
|
|
176
|
+
if (paramLocals.has(node.text)) return;
|
|
177
|
+
const initializer = consts.get(node.text);
|
|
178
|
+
if (initializer) {
|
|
179
|
+
// A sibling resource is a reference to it, not a read of its props.
|
|
180
|
+
if (ts.isNewExpression(unwrap(initializer)) || followed.has(node.text)) return;
|
|
181
|
+
followed.add(node.text);
|
|
182
|
+
visit(initializer);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (host.values.has(node.text)) found = "host-value";
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (ts.isPropertyAssignment(node)) {
|
|
190
|
+
visit(node.initializer);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// A tag is an intrinsic the lexicon registered, and only its
|
|
195
|
+
// interpolations are read.
|
|
196
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
197
|
+
visit(node.template);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ts.forEachChild(node, visit);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
visit(expr);
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
|
|
120
208
|
/**
|
|
121
209
|
* Path → build-parameter origin for one resource's props object literal.
|
|
122
210
|
*
|
|
@@ -129,16 +217,27 @@ function readParams(
|
|
|
129
217
|
* A spread is attributed to the object it spreads INTO, at that object's own
|
|
130
218
|
* path (the entity root for a top-level spread), since which keys it
|
|
131
219
|
* contributes is not knowable here.
|
|
220
|
+
*
|
|
221
|
+
* With `host`, a path whose expression reads host code ({@link hostReadOf})
|
|
222
|
+
* is recorded `host` instead, and that outranks a build parameter it also
|
|
223
|
+
* reads: the value is what host code returned, whatever the parameter was.
|
|
132
224
|
*/
|
|
133
225
|
export function collectParamDependencies(
|
|
134
226
|
props: ts.ObjectLiteralExpression,
|
|
135
227
|
consts: ReadonlyMap<string, ts.Expression>,
|
|
136
228
|
paramLocals: ReadonlySet<string>,
|
|
229
|
+
host?: HostBindings,
|
|
137
230
|
): Record<string, PathOrigin> {
|
|
138
231
|
const out: Record<string, PathOrigin> = {};
|
|
139
|
-
if (paramLocals.size === 0) return out;
|
|
232
|
+
if (paramLocals.size === 0 && !host) return out;
|
|
140
233
|
|
|
141
234
|
const record = (path: string, expr: ts.Expression): void => {
|
|
235
|
+
if (out[path]?.kind === "host") return;
|
|
236
|
+
const reason = host ? hostReadOf(expr, consts, paramLocals, host) : undefined;
|
|
237
|
+
if (reason) {
|
|
238
|
+
out[path] = { kind: "host", reason };
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
142
241
|
const found = new Set<string>();
|
|
143
242
|
readParams(expr, consts, paramLocals, found);
|
|
144
243
|
if (found.size === 0) return;
|
|
@@ -288,6 +387,61 @@ function readCompositeParameters(
|
|
|
288
387
|
visit(expr);
|
|
289
388
|
}
|
|
290
389
|
|
|
390
|
+
/**
|
|
391
|
+
* The composite call an interpreted factory body was reached from (chant
|
|
392
|
+
* #3597): the `WebApp({ replicas: 3 })` expression in the author's source and
|
|
393
|
+
* the absolute path of the file it is written in. The node must come from a
|
|
394
|
+
* source file parsed with parent pointers, which is how `fold-import.ts`
|
|
395
|
+
* parses every file it interprets.
|
|
396
|
+
*/
|
|
397
|
+
export interface CompositeCallSite {
|
|
398
|
+
file: string;
|
|
399
|
+
node: ts.CallExpression;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** A node's start as a 1-based {@link SourceLocation}. */
|
|
403
|
+
export function sourceLocationOf(file: string, node: ts.Node): SourceLocation {
|
|
404
|
+
const sourceFile = node.getSourceFile();
|
|
405
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
406
|
+
return { file, line: line + 1, column: character + 1 };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Where `parameter` was written at `call`: the property at that path in the
|
|
411
|
+
* call's object-literal argument, descending nested object literals segment by
|
|
412
|
+
* segment, or the call itself when there is no such property.
|
|
413
|
+
*
|
|
414
|
+
* The fallback covers a parameter the call leaves out, one that arrives through
|
|
415
|
+
* a spread, and an argument that is not an object literal. In each of those the
|
|
416
|
+
* call is the nearest place that governs the value, and no `text` is given,
|
|
417
|
+
* because no property at the call says what it is.
|
|
418
|
+
*/
|
|
419
|
+
export function argumentLocation(call: CompositeCallSite, parameter: string): ArgumentLocation {
|
|
420
|
+
const fallback: ArgumentLocation = { parameter, ...sourceLocationOf(call.file, call.node) };
|
|
421
|
+
const first = call.node.arguments[0];
|
|
422
|
+
if (!first) return fallback;
|
|
423
|
+
let object: ts.Expression = unwrap(first);
|
|
424
|
+
let found: ts.ObjectLiteralElementLike | undefined;
|
|
425
|
+
for (const segment of parameter.split(".")) {
|
|
426
|
+
if (!ts.isObjectLiteralExpression(object)) return fallback;
|
|
427
|
+
found = undefined;
|
|
428
|
+
// Last write wins, as it does when the object literal is evaluated.
|
|
429
|
+
for (const member of object.properties) {
|
|
430
|
+
if (ts.isShorthandPropertyAssignment(member) && member.name.text === segment) found = member;
|
|
431
|
+
else if (ts.isPropertyAssignment(member) && literalName(member.name) === segment) found = member;
|
|
432
|
+
}
|
|
433
|
+
if (!found) return fallback;
|
|
434
|
+
object = ts.isPropertyAssignment(found) ? unwrap(found.initializer) : found.name;
|
|
435
|
+
}
|
|
436
|
+
if (!found) return fallback;
|
|
437
|
+
return { parameter, ...sourceLocationOf(call.file, found), text: found.getText() };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** {@link argumentLocation} for each of `parameters`, in order, or nothing when no call was seen. */
|
|
441
|
+
function argumentLocations(call: CompositeCallSite | undefined, parameters: readonly string[]): ArgumentLocation[] | undefined {
|
|
442
|
+
return call ? parameters.map((parameter) => argumentLocation(call, parameter)) : undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
291
445
|
/**
|
|
292
446
|
* Path → composite origin for one `new Type({...})` inside an interpreted
|
|
293
447
|
* composite factory body (chant #2161).
|
|
@@ -304,12 +458,16 @@ function readCompositeParameters(
|
|
|
304
458
|
* something under here", while `composite-literal` would be a claim that every
|
|
305
459
|
* key it brings is fixed. That claim is left unmade, and those paths come back
|
|
306
460
|
* `unknown` from ../fold-provenance.ts instead.
|
|
461
|
+
*
|
|
462
|
+
* `call` (chant #3597), when the fold saw the composite call, adds where each
|
|
463
|
+
* parameter's argument was written there, in the same pass.
|
|
307
464
|
*/
|
|
308
465
|
export function collectCompositeOrigins(
|
|
309
466
|
props: ts.ObjectLiteralExpression,
|
|
310
467
|
consts: ReadonlyMap<string, ts.Expression>,
|
|
311
468
|
scope: CompositeParamScope,
|
|
312
469
|
composite: string,
|
|
470
|
+
call?: CompositeCallSite,
|
|
313
471
|
): Record<string, PathOrigin> {
|
|
314
472
|
const out: Record<string, PathOrigin> = {};
|
|
315
473
|
|
|
@@ -327,7 +485,7 @@ export function collectCompositeOrigins(
|
|
|
327
485
|
existing && existing.kind === "composite-parameter"
|
|
328
486
|
? [...new Set([...existing.parameters, ...parameters])].sort()
|
|
329
487
|
: parameters;
|
|
330
|
-
out[path] =
|
|
488
|
+
out[path] = compositeParameterOrigin(composite, merged, call);
|
|
331
489
|
return;
|
|
332
490
|
}
|
|
333
491
|
if (spread) return;
|
|
@@ -384,6 +542,7 @@ export function collectTagOrigins(
|
|
|
384
542
|
consts: ReadonlyMap<string, ts.Expression>,
|
|
385
543
|
scope: CompositeParamScope,
|
|
386
544
|
composite: string,
|
|
545
|
+
call?: CompositeCallSite,
|
|
387
546
|
): Record<string, PathOrigin> {
|
|
388
547
|
const out: Record<string, PathOrigin> = {};
|
|
389
548
|
|
|
@@ -395,7 +554,7 @@ export function collectTagOrigins(
|
|
|
395
554
|
const existing = out[path];
|
|
396
555
|
const merged = new Set(existing?.kind === "composite-parameter" ? existing.parameters : []);
|
|
397
556
|
for (const parameter of found) merged.add(parameter);
|
|
398
|
-
out[path] =
|
|
557
|
+
out[path] = compositeParameterOrigin(composite, [...merged].sort(), call);
|
|
399
558
|
}
|
|
400
559
|
});
|
|
401
560
|
|
|
@@ -406,3 +565,13 @@ export function collectTagOrigins(
|
|
|
406
565
|
}
|
|
407
566
|
return out;
|
|
408
567
|
}
|
|
568
|
+
|
|
569
|
+
/** A `composite-parameter` origin, with its argument locations when the call was seen. */
|
|
570
|
+
function compositeParameterOrigin(
|
|
571
|
+
composite: string,
|
|
572
|
+
parameters: string[],
|
|
573
|
+
call: CompositeCallSite | undefined,
|
|
574
|
+
): PathOrigin {
|
|
575
|
+
const locations = argumentLocations(call, parameters);
|
|
576
|
+
return { kind: "composite-parameter", composite, parameters, ...(locations ? { arguments: locations } : {}) };
|
|
577
|
+
}
|
|
@@ -56,3 +56,41 @@ describe("SPEC_VERSION is declared on the package entry (chant#2424)", () => {
|
|
|
56
56
|
expect(chant.SPEC_VERSION).toBe(SPEC_VERSION);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* chant#3598 — the conformance adapter reads fold provenance (spec 2.2) off
|
|
62
|
+
* `foldProject`'s verdicts, and maps or recomputes it with the functions
|
|
63
|
+
* below. All of them, and the types the verdict field is written in, have to
|
|
64
|
+
* be reachable from this namespace.
|
|
65
|
+
*/
|
|
66
|
+
describe("fold provenance is on the package entry (chant#3598)", () => {
|
|
67
|
+
test("the functions are exported", () => {
|
|
68
|
+
for (const name of [
|
|
69
|
+
"foldProject",
|
|
70
|
+
"foldProvenanceOfEntities",
|
|
71
|
+
"foldProvenanceOfEntity",
|
|
72
|
+
"classifyFieldOrigin",
|
|
73
|
+
"emittedFieldPaths",
|
|
74
|
+
"describeFoldFieldOrigin",
|
|
75
|
+
"getProvenance",
|
|
76
|
+
] as const) {
|
|
77
|
+
expect(typeof chant[name]).toBe("function");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a verdict's foldProvenance is typed in the exported shapes", () => {
|
|
82
|
+
const origins: chant.FoldFieldOrigin[] = [
|
|
83
|
+
{ kind: "composite-parameter", composite: "C", instance: "i", parameters: ["p"] },
|
|
84
|
+
{ kind: "composite-literal", composite: "C" },
|
|
85
|
+
{ kind: "direct" },
|
|
86
|
+
{ kind: "unknown", reason: "no-provenance" satisfies chant.UnknownOriginReason },
|
|
87
|
+
];
|
|
88
|
+
const record: chant.EntityFoldProvenance = { fields: Object.fromEntries(origins.map((o, i) => [`f${i}`, o])) };
|
|
89
|
+
const verdict: chant.FoldProjectVerdict = {
|
|
90
|
+
verdict: "fold",
|
|
91
|
+
tentative: "fold",
|
|
92
|
+
foldProvenance: { e: record } satisfies chant.FoldProvenance,
|
|
93
|
+
};
|
|
94
|
+
expect(Object.keys(verdict.foldProvenance!.e.fields)).toEqual(["f0", "f1", "f2", "f3"]);
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/fold/subset.ts
CHANGED
|
@@ -179,7 +179,7 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
179
179
|
* `spec/VERSION` in the specification repository carries the same string, and
|
|
180
180
|
* the conformance adapter reads this one to fill its `specVersion` field.
|
|
181
181
|
*
|
|
182
|
-
* ## Why
|
|
182
|
+
* ## Why 2.3, and what each version since 1.0 asked for (chant#2445)
|
|
183
183
|
*
|
|
184
184
|
* Raising this is the last step of adopting a rule set, so each version was
|
|
185
185
|
* checked against what chant does rather than against whether the suite is
|
|
@@ -220,6 +220,21 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
220
220
|
* - **1.8** added `ι = executing`, and chant carries it as of `0.72.3`
|
|
221
221
|
* (#2455) — `FoldProjectOptions.executing`, mutually exclusive with
|
|
222
222
|
* `sandbox`. `open` stays the default and stays strict (#2453).
|
|
223
|
+
* - **2.2** added obligations to `F-Obs-Provenance` for an implementation that
|
|
224
|
+
* claims provenance: one origin per emitted path of four kinds, the
|
|
225
|
+
* innermost writer wins, and an origin that cannot be determined is reported
|
|
226
|
+
* as unknown, never as direct. chant claims provenance, so this is the
|
|
227
|
+
* version whose obligations it takes on. 2.2 also widens two rules to what
|
|
228
|
+
* chant already did: a call-bound local exported by name (`export { x }`)
|
|
229
|
+
* resolves by F-Call as the single declarator does, and a same-file call's
|
|
230
|
+
* result read inside an F-Call argument resolves once per file (#3329).
|
|
231
|
+
* - **2.3** says a value host code produced, written at an export or into a
|
|
232
|
+
* field, has an unknown origin and never a direct one. chant reports
|
|
233
|
+
* `host-call` and `host-value` for resource fields since
|
|
234
|
+
* typescript-as-data#248. 2.3 also lets `F-Host-Composite` interpret a
|
|
235
|
+
* composite an active package publishes as TypeScript source, which chant
|
|
236
|
+
* has done since #3247, and adds fixtures for a call result read inside an
|
|
237
|
+
* argument, which chant passes since #3610.
|
|
223
238
|
*
|
|
224
239
|
* `scripts/check-docs-citations.ts` refuses to run when this constant and the
|
|
225
240
|
* rules it loads disagree, so this and the `@intentius/tsad-conformance`
|
|
@@ -231,7 +246,7 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
231
246
|
* Until chant#2470 the rules came from a commit `docs-check.yml` checked out;
|
|
232
247
|
* now they come from the published package, so bumping it is what moves this.
|
|
233
248
|
*/
|
|
234
|
-
export const SPEC_VERSION = "2.
|
|
249
|
+
export const SPEC_VERSION = "2.3";
|
|
235
250
|
|
|
236
251
|
/** The two EVL rule ids a shape violation can be attributed to. */
|
|
237
252
|
export type SubsetRuleId = "EVL001" | "EVL003";
|