@intentius/chant 0.108.1 → 0.109.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 +30 -3
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +10 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +35 -2
- package/dist/fold-provenance.d.ts.map +1 -1
- package/dist/provenance.d.ts +38 -0
- package/dist/provenance.d.ts.map +1 -1
- package/package.json +1 -1
- 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-import.ts +251 -9
- package/src/discovery/fold-package-composite.test.ts +14 -1
- package/src/discovery/fold-project-provenance.test.ts +532 -0
- package/src/discovery/param-deps.test.ts +120 -1
- package/src/discovery/param-deps.ts +73 -3
- package/src/fold/subset-public-export.test.ts +38 -0
- package/src/fold/subset.ts +10 -2
- package/src/fold-provenance.test.ts +179 -16
- package/src/fold-provenance.ts +136 -11
- package/src/provenance.ts +46 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
|
-
import type { PathOrigin } from "../provenance";
|
|
2
|
+
import type { ArgumentLocation, PathOrigin, SourceLocation } from "../provenance";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Which build parameters an authored props expression READS, per path (chant
|
|
@@ -288,6 +288,61 @@ function readCompositeParameters(
|
|
|
288
288
|
visit(expr);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The composite call an interpreted factory body was reached from (chant
|
|
293
|
+
* #3597): the `WebApp({ replicas: 3 })` expression in the author's source and
|
|
294
|
+
* the absolute path of the file it is written in. The node must come from a
|
|
295
|
+
* source file parsed with parent pointers, which is how `fold-import.ts`
|
|
296
|
+
* parses every file it interprets.
|
|
297
|
+
*/
|
|
298
|
+
export interface CompositeCallSite {
|
|
299
|
+
file: string;
|
|
300
|
+
node: ts.CallExpression;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** A node's start as a 1-based {@link SourceLocation}. */
|
|
304
|
+
export function sourceLocationOf(file: string, node: ts.Node): SourceLocation {
|
|
305
|
+
const sourceFile = node.getSourceFile();
|
|
306
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
307
|
+
return { file, line: line + 1, column: character + 1 };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Where `parameter` was written at `call`: the property at that path in the
|
|
312
|
+
* call's object-literal argument, descending nested object literals segment by
|
|
313
|
+
* segment, or the call itself when there is no such property.
|
|
314
|
+
*
|
|
315
|
+
* The fallback covers a parameter the call leaves out, one that arrives through
|
|
316
|
+
* a spread, and an argument that is not an object literal. In each of those the
|
|
317
|
+
* call is the nearest place that governs the value, and no `text` is given,
|
|
318
|
+
* because no property at the call says what it is.
|
|
319
|
+
*/
|
|
320
|
+
export function argumentLocation(call: CompositeCallSite, parameter: string): ArgumentLocation {
|
|
321
|
+
const fallback: ArgumentLocation = { parameter, ...sourceLocationOf(call.file, call.node) };
|
|
322
|
+
const first = call.node.arguments[0];
|
|
323
|
+
if (!first) return fallback;
|
|
324
|
+
let object: ts.Expression = unwrap(first);
|
|
325
|
+
let found: ts.ObjectLiteralElementLike | undefined;
|
|
326
|
+
for (const segment of parameter.split(".")) {
|
|
327
|
+
if (!ts.isObjectLiteralExpression(object)) return fallback;
|
|
328
|
+
found = undefined;
|
|
329
|
+
// Last write wins, as it does when the object literal is evaluated.
|
|
330
|
+
for (const member of object.properties) {
|
|
331
|
+
if (ts.isShorthandPropertyAssignment(member) && member.name.text === segment) found = member;
|
|
332
|
+
else if (ts.isPropertyAssignment(member) && literalName(member.name) === segment) found = member;
|
|
333
|
+
}
|
|
334
|
+
if (!found) return fallback;
|
|
335
|
+
object = ts.isPropertyAssignment(found) ? unwrap(found.initializer) : found.name;
|
|
336
|
+
}
|
|
337
|
+
if (!found) return fallback;
|
|
338
|
+
return { parameter, ...sourceLocationOf(call.file, found), text: found.getText() };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** {@link argumentLocation} for each of `parameters`, in order, or nothing when no call was seen. */
|
|
342
|
+
function argumentLocations(call: CompositeCallSite | undefined, parameters: readonly string[]): ArgumentLocation[] | undefined {
|
|
343
|
+
return call ? parameters.map((parameter) => argumentLocation(call, parameter)) : undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
291
346
|
/**
|
|
292
347
|
* Path → composite origin for one `new Type({...})` inside an interpreted
|
|
293
348
|
* composite factory body (chant #2161).
|
|
@@ -304,12 +359,16 @@ function readCompositeParameters(
|
|
|
304
359
|
* something under here", while `composite-literal` would be a claim that every
|
|
305
360
|
* key it brings is fixed. That claim is left unmade, and those paths come back
|
|
306
361
|
* `unknown` from ../fold-provenance.ts instead.
|
|
362
|
+
*
|
|
363
|
+
* `call` (chant #3597), when the fold saw the composite call, adds where each
|
|
364
|
+
* parameter's argument was written there, in the same pass.
|
|
307
365
|
*/
|
|
308
366
|
export function collectCompositeOrigins(
|
|
309
367
|
props: ts.ObjectLiteralExpression,
|
|
310
368
|
consts: ReadonlyMap<string, ts.Expression>,
|
|
311
369
|
scope: CompositeParamScope,
|
|
312
370
|
composite: string,
|
|
371
|
+
call?: CompositeCallSite,
|
|
313
372
|
): Record<string, PathOrigin> {
|
|
314
373
|
const out: Record<string, PathOrigin> = {};
|
|
315
374
|
|
|
@@ -327,7 +386,7 @@ export function collectCompositeOrigins(
|
|
|
327
386
|
existing && existing.kind === "composite-parameter"
|
|
328
387
|
? [...new Set([...existing.parameters, ...parameters])].sort()
|
|
329
388
|
: parameters;
|
|
330
|
-
out[path] =
|
|
389
|
+
out[path] = compositeParameterOrigin(composite, merged, call);
|
|
331
390
|
return;
|
|
332
391
|
}
|
|
333
392
|
if (spread) return;
|
|
@@ -384,6 +443,7 @@ export function collectTagOrigins(
|
|
|
384
443
|
consts: ReadonlyMap<string, ts.Expression>,
|
|
385
444
|
scope: CompositeParamScope,
|
|
386
445
|
composite: string,
|
|
446
|
+
call?: CompositeCallSite,
|
|
387
447
|
): Record<string, PathOrigin> {
|
|
388
448
|
const out: Record<string, PathOrigin> = {};
|
|
389
449
|
|
|
@@ -395,7 +455,7 @@ export function collectTagOrigins(
|
|
|
395
455
|
const existing = out[path];
|
|
396
456
|
const merged = new Set(existing?.kind === "composite-parameter" ? existing.parameters : []);
|
|
397
457
|
for (const parameter of found) merged.add(parameter);
|
|
398
|
-
out[path] =
|
|
458
|
+
out[path] = compositeParameterOrigin(composite, [...merged].sort(), call);
|
|
399
459
|
}
|
|
400
460
|
});
|
|
401
461
|
|
|
@@ -406,3 +466,13 @@ export function collectTagOrigins(
|
|
|
406
466
|
}
|
|
407
467
|
return out;
|
|
408
468
|
}
|
|
469
|
+
|
|
470
|
+
/** A `composite-parameter` origin, with its argument locations when the call was seen. */
|
|
471
|
+
function compositeParameterOrigin(
|
|
472
|
+
composite: string,
|
|
473
|
+
parameters: string[],
|
|
474
|
+
call: CompositeCallSite | undefined,
|
|
475
|
+
): PathOrigin {
|
|
476
|
+
const locations = argumentLocations(call, parameters);
|
|
477
|
+
return { kind: "composite-parameter", composite, parameters, ...(locations ? { arguments: locations } : {}) };
|
|
478
|
+
}
|
|
@@ -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.2, 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,14 @@ 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).
|
|
223
231
|
*
|
|
224
232
|
* `scripts/check-docs-citations.ts` refuses to run when this constant and the
|
|
225
233
|
* rules it loads disagree, so this and the `@intentius/tsad-conformance`
|
|
@@ -231,7 +239,7 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
231
239
|
* Until chant#2470 the rules came from a commit `docs-check.yml` checked out;
|
|
232
240
|
* now they come from the published package, so bumping it is what moves this.
|
|
233
241
|
*/
|
|
234
|
-
export const SPEC_VERSION = "2.
|
|
242
|
+
export const SPEC_VERSION = "2.2";
|
|
235
243
|
|
|
236
244
|
/** The two EVL rule ids a shape violation can be attributed to. */
|
|
237
245
|
export type SubsetRuleId = "EVL001" | "EVL003";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
classifyFieldOrigin,
|
|
19
19
|
emittedFieldPaths,
|
|
20
20
|
foldProvenanceOfEntity,
|
|
21
|
+
describeCompositeArguments,
|
|
21
22
|
describeFoldFieldOrigin,
|
|
22
23
|
resolveDeepDrift,
|
|
23
24
|
resolveDriftedField,
|
|
@@ -76,7 +77,7 @@ describe("classifyFieldOrigin", () => {
|
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
test("a recorded composite literal says which composite fixes it", () => {
|
|
79
|
-
expect(classifyFieldOrigin({ kind: "composite-literal", composite: "WebService" }, composite)).
|
|
80
|
+
expect(classifyFieldOrigin({ kind: "composite-literal", composite: "WebService" }, composite)).toMatchObject({
|
|
80
81
|
kind: "composite-literal",
|
|
81
82
|
composite: "WebService",
|
|
82
83
|
instance: "web",
|
|
@@ -244,6 +245,126 @@ describe("resolveDriftedField", () => {
|
|
|
244
245
|
});
|
|
245
246
|
});
|
|
246
247
|
|
|
248
|
+
describe("the drift report's origin line (#3597)", () => {
|
|
249
|
+
const call = { file: "/p/src/app.ts", line: 10, column: 20 };
|
|
250
|
+
const compositeProv: EntityProvenance = {
|
|
251
|
+
sourceFile: "/p/src/app.ts",
|
|
252
|
+
composite: "WebApp",
|
|
253
|
+
compositeInstance: "web",
|
|
254
|
+
compositeCall: call,
|
|
255
|
+
};
|
|
256
|
+
const base = { entity: "webDeployment", entityType: "Deployment", root: "/p", declared: 3, live: 5 };
|
|
257
|
+
|
|
258
|
+
test("a composite parameter names the call, the argument as written, and its file and line", () => {
|
|
259
|
+
const resolved = resolveDriftedField({
|
|
260
|
+
...base,
|
|
261
|
+
path: "spec.replicas",
|
|
262
|
+
origin: {
|
|
263
|
+
kind: "composite-parameter",
|
|
264
|
+
composite: "WebApp",
|
|
265
|
+
parameters: ["replicas"],
|
|
266
|
+
arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 12, column: 3, text: "replicas: 3" }],
|
|
267
|
+
},
|
|
268
|
+
provenance: compositeProv,
|
|
269
|
+
});
|
|
270
|
+
expect(resolved.summary).toBe(
|
|
271
|
+
"spec.replicas on Deployment webDeployment comes from WebApp({ replicas: 3 }) at src/app.ts:12",
|
|
272
|
+
);
|
|
273
|
+
expect(resolved.resolution.description).toContain("the `web` call of composite WebApp at src/app.ts:12");
|
|
274
|
+
// The origin object carries the locations, for --json.
|
|
275
|
+
expect(resolved.origin).toEqual({
|
|
276
|
+
kind: "composite-parameter",
|
|
277
|
+
composite: "WebApp",
|
|
278
|
+
instance: "web",
|
|
279
|
+
parameters: ["replicas"],
|
|
280
|
+
call,
|
|
281
|
+
arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 12, column: 3, text: "replicas: 3" }],
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a nested parameter is shown nested, and a parameter the call does not write says so", () => {
|
|
286
|
+
expect(describeCompositeArguments("WebService", [
|
|
287
|
+
{ parameter: "iam.path", file: "/p/a.ts", line: 4, column: 9, text: 'path: "/x/"' },
|
|
288
|
+
])).toBe('WebService({ iam: { path: "/x/" } })');
|
|
289
|
+
|
|
290
|
+
const resolved = resolveDriftedField({
|
|
291
|
+
...base,
|
|
292
|
+
path: "spec.template.spec.containers[#web].image",
|
|
293
|
+
origin: {
|
|
294
|
+
kind: "composite-parameter",
|
|
295
|
+
composite: "WebApp",
|
|
296
|
+
parameters: ["image"],
|
|
297
|
+
arguments: [{ parameter: "image", ...call }],
|
|
298
|
+
},
|
|
299
|
+
provenance: compositeProv,
|
|
300
|
+
});
|
|
301
|
+
expect(resolved.summary).toBe(
|
|
302
|
+
"spec.template.spec.containers[#web].image on Deployment webDeployment comes from WebApp(...) " +
|
|
303
|
+
"at src/app.ts:10 (parameter image, not written at the call)",
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("a composite literal says the composite fixes it, at the call", () => {
|
|
308
|
+
const resolved = resolveDriftedField({
|
|
309
|
+
...base,
|
|
310
|
+
path: "spec.strategy.type",
|
|
311
|
+
origin: { kind: "composite-literal", composite: "WebApp" },
|
|
312
|
+
provenance: compositeProv,
|
|
313
|
+
});
|
|
314
|
+
expect(resolved.summary).toBe(
|
|
315
|
+
"spec.strategy.type on Deployment webDeployment is fixed inside composite WebApp; " +
|
|
316
|
+
"no argument to the call at src/app.ts:10 moves it",
|
|
317
|
+
);
|
|
318
|
+
expect(resolved.resolution.description).toContain("fixed by the `web` call of composite WebApp at src/app.ts:10");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("a direct declaration says direct, and the resolution is today's", () => {
|
|
322
|
+
const resolved = resolveDriftedField({
|
|
323
|
+
...base,
|
|
324
|
+
entity: "worker",
|
|
325
|
+
path: "spec.replicas",
|
|
326
|
+
provenance: { sourceFile: "/p/src/app.ts" },
|
|
327
|
+
});
|
|
328
|
+
expect(resolved.origin).toEqual({ kind: "direct" });
|
|
329
|
+
expect(resolved.summary).toBe("spec.replicas on Deployment worker is declared directly in src/app.ts");
|
|
330
|
+
expect(resolved.resolution).toEqual({
|
|
331
|
+
kind: "edit-declaration",
|
|
332
|
+
sourceFile: "/p/src/app.ts",
|
|
333
|
+
description: "change the declared value of `spec.replicas` in src/app.ts from 3 to 5.",
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("an unknown origin says unknown and why, and never reads as direct", () => {
|
|
338
|
+
for (const [provenance, why] of [
|
|
339
|
+
[{ sourceFile: "/p/src/app.ts", composite: "WebApp", compositeInstance: "web" }, "did not interpret"],
|
|
340
|
+
[undefined, "recorded no provenance"],
|
|
341
|
+
] as const) {
|
|
342
|
+
const resolved = resolveDriftedField({ ...base, path: "spec.replicas", ...(provenance ? { provenance } : {}) });
|
|
343
|
+
expect(resolved.origin.kind).toBe("unknown");
|
|
344
|
+
expect(resolved.summary).toMatch(/^spec\.replicas on Deployment webDeployment has an unknown origin: /);
|
|
345
|
+
expect(resolved.summary).toContain(why);
|
|
346
|
+
expect(resolved.summary).not.toContain("direct");
|
|
347
|
+
expect(resolved.resolution.kind).toBe("fall-back");
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("a file outside the project root is printed as recorded", () => {
|
|
352
|
+
const resolved = resolveDriftedField({
|
|
353
|
+
...base,
|
|
354
|
+
root: "/elsewhere",
|
|
355
|
+
path: "spec.replicas",
|
|
356
|
+
origin: {
|
|
357
|
+
kind: "composite-parameter",
|
|
358
|
+
composite: "WebApp",
|
|
359
|
+
parameters: ["replicas"],
|
|
360
|
+
arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 12, column: 3, text: "replicas: 3" }],
|
|
361
|
+
},
|
|
362
|
+
provenance: compositeProv,
|
|
363
|
+
});
|
|
364
|
+
expect(resolved.summary).toContain("at /p/src/app.ts:12");
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
247
368
|
describe("resolveDeepDrift", () => {
|
|
248
369
|
test("reads the drifted list and nothing else", () => {
|
|
249
370
|
const verdicts = resolveDeepDrift(
|
|
@@ -399,25 +520,25 @@ describe("fold provenance over a real composite build (#2161)", () => {
|
|
|
399
520
|
// 1. A field the composite PARAMETERIZES, including a nested parameter
|
|
400
521
|
// path, one read through a body `const`, and one read from inside an
|
|
401
522
|
// array literal.
|
|
402
|
-
expect(result.foldProvenance.webBucket.fields.BucketName).
|
|
523
|
+
expect(result.foldProvenance.webBucket.fields.BucketName).toMatchObject({
|
|
403
524
|
kind: "composite-parameter",
|
|
404
525
|
composite: "WebService",
|
|
405
526
|
instance: "web",
|
|
406
527
|
parameters: ["name"],
|
|
407
528
|
});
|
|
408
|
-
expect(result.foldProvenance.webBucket.fields.Tags).
|
|
529
|
+
expect(result.foldProvenance.webBucket.fields.Tags).toMatchObject({
|
|
409
530
|
kind: "composite-parameter",
|
|
410
531
|
composite: "WebService",
|
|
411
532
|
instance: "web",
|
|
412
533
|
parameters: ["tier"],
|
|
413
534
|
});
|
|
414
|
-
expect(result.foldProvenance.webRole.fields.RoleName).
|
|
535
|
+
expect(result.foldProvenance.webRole.fields.RoleName).toMatchObject({
|
|
415
536
|
kind: "composite-parameter",
|
|
416
537
|
composite: "WebService",
|
|
417
538
|
instance: "web",
|
|
418
539
|
parameters: ["name"],
|
|
419
540
|
});
|
|
420
|
-
expect(result.foldProvenance.webRole.fields.Path).
|
|
541
|
+
expect(result.foldProvenance.webRole.fields.Path).toMatchObject({
|
|
421
542
|
kind: "composite-parameter",
|
|
422
543
|
composite: "WebService",
|
|
423
544
|
instance: "web",
|
|
@@ -425,12 +546,12 @@ describe("fold provenance over a real composite build (#2161)", () => {
|
|
|
425
546
|
});
|
|
426
547
|
|
|
427
548
|
// 2. A field the composite FIXES.
|
|
428
|
-
expect(result.foldProvenance.webBucket.fields["VersioningConfiguration.Status"]).
|
|
549
|
+
expect(result.foldProvenance.webBucket.fields["VersioningConfiguration.Status"]).toMatchObject({
|
|
429
550
|
kind: "composite-literal",
|
|
430
551
|
composite: "WebService",
|
|
431
552
|
instance: "web",
|
|
432
553
|
});
|
|
433
|
-
expect(result.foldProvenance.webRole.fields.Description).
|
|
554
|
+
expect(result.foldProvenance.webRole.fields.Description).toMatchObject({
|
|
434
555
|
kind: "composite-literal",
|
|
435
556
|
composite: "WebService",
|
|
436
557
|
instance: "web",
|
|
@@ -507,6 +628,43 @@ describe("fold provenance over a real composite build (#2161)", () => {
|
|
|
507
628
|
// A direct declaration: today's behaviour, unchanged and unremarked.
|
|
508
629
|
expect(verdicts[3].resolution.kind).toBe("edit-declaration");
|
|
509
630
|
});
|
|
631
|
+
|
|
632
|
+
test("the build records where the composite call and each argument were written (#3597)", async () => {
|
|
633
|
+
const result = await build(srcDir, [namesSerializer], undefined, {
|
|
634
|
+
fold: true,
|
|
635
|
+
lexicons: [LEXICON_NAME],
|
|
636
|
+
});
|
|
637
|
+
expect(result.errors).toEqual([]);
|
|
638
|
+
const main = join(srcDir, "main.ts");
|
|
639
|
+
const lines = MAIN.split("\n");
|
|
640
|
+
const lineOf = (needle: string) => lines.findIndex((l) => l.includes(needle)) + 1;
|
|
641
|
+
const columnOf = (needle: string) => (lines.find((l) => l.includes(needle)) as string).indexOf(needle) + 1;
|
|
642
|
+
const call = { file: main, line: lineOf("WebService({"), column: columnOf("WebService({") };
|
|
643
|
+
|
|
644
|
+
expect(result.foldProvenance.webBucket.compositeCall).toEqual(call);
|
|
645
|
+
expect(result.foldProvenance.webRole.compositeCall).toEqual(call);
|
|
646
|
+
expect(result.foldProvenance.webBucket.fields.BucketName).toEqual({
|
|
647
|
+
kind: "composite-parameter",
|
|
648
|
+
composite: "WebService",
|
|
649
|
+
instance: "web",
|
|
650
|
+
parameters: ["name"],
|
|
651
|
+
call,
|
|
652
|
+
arguments: [{ parameter: "name", file: main, line: call.line, column: columnOf('name: "data"'), text: 'name: "data"' }],
|
|
653
|
+
});
|
|
654
|
+
expect(result.foldProvenance.webRole.fields.Path).toMatchObject({
|
|
655
|
+
arguments: [{ parameter: "iam.path", line: call.line, column: columnOf('path: "/service/"'), text: 'path: "/service/"' }],
|
|
656
|
+
});
|
|
657
|
+
expect(result.foldProvenance.webBucket.fields["VersioningConfiguration.Status"]).toEqual({
|
|
658
|
+
kind: "composite-literal",
|
|
659
|
+
composite: "WebService",
|
|
660
|
+
instance: "web",
|
|
661
|
+
call,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// A direct declaration and an uninterpreted composite record no call.
|
|
665
|
+
expect(result.foldProvenance.logs.compositeCall).toBeUndefined();
|
|
666
|
+
expect(result.foldProvenance.legacyBucket.compositeCall).toBeUndefined();
|
|
667
|
+
});
|
|
510
668
|
});
|
|
511
669
|
|
|
512
670
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -604,18 +762,23 @@ describe("fold provenance through a tagged template in a composite factory (#321
|
|
|
604
762
|
const at = (parameters: string[]) => ({ kind: "composite-parameter", composite: "EventsTable", instance: "clicks", parameters });
|
|
605
763
|
const fixed = { kind: "composite-literal", composite: "EventsTable", instance: "clicks" };
|
|
606
764
|
|
|
607
|
-
expect(events.fields.name).
|
|
608
|
-
expect(events.fields.ttl).
|
|
609
|
-
expect(events.fields.ddl).
|
|
610
|
-
expect(events.fields.engine).
|
|
611
|
-
expect(events.fields.orderBy).
|
|
765
|
+
expect(events.fields.name).toMatchObject(at(["name"]));
|
|
766
|
+
expect(events.fields.ttl).toMatchObject(at(["ttlDays"]));
|
|
767
|
+
expect(events.fields.ddl).toMatchObject(at(["name", "ttlDays"]));
|
|
768
|
+
expect(events.fields.engine).toMatchObject(fixed);
|
|
769
|
+
expect(events.fields.orderBy).toMatchObject(fixed);
|
|
770
|
+
// #3597 — the argument behind a tag-fed field, where the call wrote it.
|
|
771
|
+
expect(events.fields.ttl).toMatchObject({ arguments: [{ parameter: "ttlDays", line: 6, text: "ttlDays: 30" }] });
|
|
772
|
+
expect(events.fields.ddl).toMatchObject({
|
|
773
|
+
arguments: [{ parameter: "name", text: 'name: "clicks"' }, { parameter: "ttlDays", text: "ttlDays: 30" }],
|
|
774
|
+
});
|
|
612
775
|
|
|
613
776
|
// Through a body `const`; an interpolated literal string is fixed; an
|
|
614
777
|
// interpolated sibling entity is wiring, so it governs nothing.
|
|
615
|
-
expect(rollup.fields.name).
|
|
616
|
-
expect(rollup.fields.engine).
|
|
617
|
-
expect(rollup.fields.comment).
|
|
618
|
-
expect(rollup.fields.ddl).
|
|
778
|
+
expect(rollup.fields.name).toMatchObject(at(["name"]));
|
|
779
|
+
expect(rollup.fields.engine).toMatchObject(fixed);
|
|
780
|
+
expect(rollup.fields.comment).toMatchObject(fixed);
|
|
781
|
+
expect(rollup.fields.ddl).toMatchObject(at(["name"]));
|
|
619
782
|
|
|
620
783
|
expect(result.foldProvenance.audit.fields.ttl).toBeUndefined();
|
|
621
784
|
expect(result.foldProvenance.audit.fields.name).toEqual({ kind: "direct" });
|