@intentius/chant 0.19.0 → 0.20.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-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +210 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +226 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +102 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +81 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/terraform/aws-resources.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +119 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +61 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +485 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1026 -0
- package/src/discovery/fold-import.ts +1805 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +233 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +535 -0
- package/src/fold/fold.ts +648 -0
- package/src/fold/subset-doc-parity.test.ts +183 -0
- package/src/fold/subset.test.ts +241 -0
- package/src/fold/subset.ts +302 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +14 -0
- package/src/lexicon-schema.ts +3 -1
- package/src/lexicon.ts +82 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
- package/src/terraform/adopt-state.test.ts +1 -1
- package/src/terraform/aws-resources.test.ts +11 -4
- package/src/terraform/aws-resources.ts +60 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { collectConsts } from "./fold";
|
|
7
|
+
import { findSubsetViolation } from "./subset";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* subset-doc-parity.test.ts — chant #1062 (epic #1019).
|
|
11
|
+
*
|
|
12
|
+
* `docs/.../concepts/typescript-as-data.mdx`'s "Supported Patterns" and
|
|
13
|
+
* "Unsupported Patterns" sections are prose over `findSubsetViolation`
|
|
14
|
+
* (./subset.ts), the real, code-defined classifier. #1062 exists because
|
|
15
|
+
* that prose already drifted from the code once (see subset.ts's own
|
|
16
|
+
* module doc, and #1061's fix) and nothing stopped it.
|
|
17
|
+
*
|
|
18
|
+
* Full generation was considered and rejected: `findSubsetViolation` is a
|
|
19
|
+
* recursive-descent classifier over `ts.Node` kinds, not a flat table — its
|
|
20
|
+
* value is precisely the nuance a table would lose (flow-insensitive
|
|
21
|
+
* short-circuit handling, opaque tagged-template interiors, which EVL rule
|
|
22
|
+
* id a rejection maps to, …). Mechanically reverse-engineering that nuance
|
|
23
|
+
* from the function body would mean either emitting a bare list of
|
|
24
|
+
* `ts.SyntaxKind` names (useless to a reader) or writing a second, brittle
|
|
25
|
+
* introspector that breaks on any harmless refactor of subset.ts — a worse
|
|
26
|
+
* failure mode than the one this issue exists to fix.
|
|
27
|
+
*
|
|
28
|
+
* Instead: this test pulls each pattern's example STRAIGHT OUT of the doc's
|
|
29
|
+
* own fenced code block (by heading, via {@link extractFencedBlock}) — no
|
|
30
|
+
* second copy of the snippet lives in this file — and runs it through
|
|
31
|
+
* `findSubsetViolation` for real. A heading that's renamed or removed fails
|
|
32
|
+
* loudly (`heading not found`); a snippet whose real verdict no longer
|
|
33
|
+
* matches what the doc claims for it fails just as loudly. That is the
|
|
34
|
+
* concrete failure mode from the day this issue was filed, closed for good.
|
|
35
|
+
*
|
|
36
|
+
* Scope: only patterns `findSubsetViolation` itself decides (EVL001/EVL003 —
|
|
37
|
+
* see subset.ts's module doc for why those are the two it owns). Left out,
|
|
38
|
+
* deliberately:
|
|
39
|
+
* - "Control flow around resources" (EVL002) and the `let`/`var`, `class`,
|
|
40
|
+
* `require()`, top-level `await`, decorator bullets under "Other
|
|
41
|
+
* unsupported patterns" — these are rejected at `foldModule`'s top-level
|
|
42
|
+
* statement scan, before any expression ever reaches
|
|
43
|
+
* `findSubsetViolation`. Not this module's concern, and not real,
|
|
44
|
+
* parseable TypeScript in the doc's own illustrative snippet (a bare
|
|
45
|
+
* `export` inside an `if` block isn't valid syntax to begin with).
|
|
46
|
+
* - "Import and re-export" / "Cross-file resource references" / "Typed
|
|
47
|
+
* property-kind constructors" are exercised below, but only prove the
|
|
48
|
+
* trivial case: `findSubsetViolation` treats every identifier as
|
|
49
|
+
* shape-valid regardless of whether it resolves locally, across a
|
|
50
|
+
* module boundary, or not at all (subset.ts's module doc, point 1) — a
|
|
51
|
+
* documented, intentional asymmetry, not something this guard can
|
|
52
|
+
* usefully narrow further without a binding resolver of its own.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
56
|
+
const docPath = join(repoRoot, "docs", "src", "content", "docs", "concepts", "typescript-as-data.mdx");
|
|
57
|
+
const doc = readFileSync(docPath, "utf-8");
|
|
58
|
+
|
|
59
|
+
/** Pull the first ```typescript fenced block following `### {heading}`. */
|
|
60
|
+
function extractFencedBlock(heading: string): string {
|
|
61
|
+
const headingMarker = `### ${heading}`;
|
|
62
|
+
const headingIdx = doc.indexOf(headingMarker);
|
|
63
|
+
if (headingIdx === -1) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`subset-doc-parity: heading "${headingMarker}" not found in ${docPath} — was it renamed or removed? Update this test to match.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const fenceStart = doc.indexOf("```typescript", headingIdx);
|
|
69
|
+
if (fenceStart === -1) {
|
|
70
|
+
throw new Error(`subset-doc-parity: no \`\`\`typescript block found after "${headingMarker}"`);
|
|
71
|
+
}
|
|
72
|
+
const codeStart = doc.indexOf("\n", fenceStart) + 1;
|
|
73
|
+
const fenceEnd = doc.indexOf("```", codeStart);
|
|
74
|
+
if (fenceEnd === -1) {
|
|
75
|
+
throw new Error(`subset-doc-parity: unterminated code fence after "${headingMarker}"`);
|
|
76
|
+
}
|
|
77
|
+
return doc.slice(codeStart, fenceEnd);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Parse `source` and return its top-level `const` bindings, keyed by name. */
|
|
81
|
+
function parseConsts(source: string): Map<string, ts.Expression> {
|
|
82
|
+
const sourceFile = ts.createSourceFile("doc-example.ts", source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
83
|
+
return collectConsts(sourceFile);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The object-literal argument of `const <name> = new Type({...})`. */
|
|
87
|
+
function resourceArg(consts: Map<string, ts.Expression>, name: string): ts.Expression {
|
|
88
|
+
const init = consts.get(name);
|
|
89
|
+
if (!init || !ts.isNewExpression(init) || !init.arguments?.[0]) {
|
|
90
|
+
throw new Error(`subset-doc-parity: "${name}" did not parse as a resource declaration with an object-literal arg`);
|
|
91
|
+
}
|
|
92
|
+
return init.arguments[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("subset-doc-parity — supported patterns in typescript-as-data.mdx classify as fold-clean", () => {
|
|
96
|
+
test("Resource declarations", () => {
|
|
97
|
+
const consts = parseConsts(extractFencedBlock("Resource declarations"));
|
|
98
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("Literal values", () => {
|
|
102
|
+
const consts = parseConsts(extractFencedBlock("Literal values"));
|
|
103
|
+
expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("Const variable references", () => {
|
|
107
|
+
const consts = parseConsts(extractFencedBlock("Const variable references"));
|
|
108
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Spread from const sources", () => {
|
|
112
|
+
const consts = parseConsts(extractFencedBlock("Spread from const sources"));
|
|
113
|
+
expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("Import and re-export", () => {
|
|
117
|
+
// Trivial by construction — see this file's module doc.
|
|
118
|
+
const consts = parseConsts(extractFencedBlock("Import and re-export"));
|
|
119
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("Cross-file resource references", () => {
|
|
123
|
+
// Trivial by construction — see this file's module doc.
|
|
124
|
+
const consts = parseConsts(extractFencedBlock("Cross-file resource references"));
|
|
125
|
+
expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("Intrinsic tagged templates", () => {
|
|
129
|
+
// subset.ts treats any tagged template's TAG as shape-valid regardless
|
|
130
|
+
// of lexicon registration (that check is fold()'s job, not subset.ts's —
|
|
131
|
+
// see subset.ts's module doc, point 2) — only the interpolated values
|
|
132
|
+
// are classified, and this example's interpolation is a plain property
|
|
133
|
+
// access chain.
|
|
134
|
+
const consts = parseConsts(extractFencedBlock("Intrinsic tagged templates"));
|
|
135
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("Typed property-kind constructors", () => {
|
|
139
|
+
const consts = parseConsts(extractFencedBlock("Typed property-kind constructors"));
|
|
140
|
+
expect(findSubsetViolation(resourceArg(consts, "config"))).toBeUndefined();
|
|
141
|
+
expect(findSubsetViolation(resourceArg(consts, "access"))).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("Nullish coalescing for defaults", () => {
|
|
145
|
+
// Not a standalone statement in the doc (a single object-literal
|
|
146
|
+
// property, deliberately shown as a fragment) — wrapped in an object
|
|
147
|
+
// literal to parse, which also happens to be exactly the shape
|
|
148
|
+
// `findSubsetViolation` classifies a resource prop through.
|
|
149
|
+
const fragment = extractFencedBlock("Nullish coalescing for defaults").trim();
|
|
150
|
+
const consts = parseConsts(`const _wrapped = { ${fragment} };`);
|
|
151
|
+
const wrapped = consts.get("_wrapped");
|
|
152
|
+
if (!wrapped) throw new Error("subset-doc-parity: nullish-coalescing fragment failed to parse");
|
|
153
|
+
expect(findSubsetViolation(wrapped)).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("subset-doc-parity — unsupported patterns in typescript-as-data.mdx classify as rejected", () => {
|
|
158
|
+
test("Function calls as values", () => {
|
|
159
|
+
const consts = parseConsts(extractFencedBlock("Function calls as values"));
|
|
160
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("Dynamic property access", () => {
|
|
164
|
+
// A bare `const`, not a resource declaration — check the initializer
|
|
165
|
+
// directly rather than through `resourceArg`.
|
|
166
|
+
const consts = parseConsts(extractFencedBlock("Dynamic property access"));
|
|
167
|
+
const init = consts.get("name");
|
|
168
|
+
if (!init) throw new Error(`subset-doc-parity: "name" did not parse as a const declaration`);
|
|
169
|
+
expect(findSubsetViolation(init)).toBeDefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("Spread from dynamic sources", () => {
|
|
173
|
+
// Documented as EVL004 (a stricter, value-level narrowing rule subset.ts
|
|
174
|
+
// doesn't model — see subset.ts's module doc, item 3) but this
|
|
175
|
+
// particular example — spreading a CALL's result — is also rejected by
|
|
176
|
+
// subset.ts's shape classifier on its own terms: a call expression
|
|
177
|
+
// nested inside a spread is still a call expression. Asserting only
|
|
178
|
+
// "rejected", not a specific rule id, since attributing a rule id here
|
|
179
|
+
// is EVL004's job, not subset.ts's.
|
|
180
|
+
const consts = parseConsts(extractFencedBlock("Spread from dynamic sources"));
|
|
181
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { fold, foldResource, collectConsts, FoldError } from "./fold";
|
|
4
|
+
import { evl001NonLiteralExpressionRule } from "../lint/rules/evl001-non-literal-expression";
|
|
5
|
+
import { evl003DynamicPropertyAccessRule } from "../lint/rules/evl003-dynamic-property-access";
|
|
6
|
+
import { evl004SpreadNonConstRule } from "../lint/rules/evl004-spread-non-const";
|
|
7
|
+
import type { LintContext } from "../lint/rule";
|
|
8
|
+
import type { IntrinsicDef } from "../lexicon";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* subset.test.ts — the equivalence property test for #1024: for a corpus of
|
|
12
|
+
* supported AND unsupported expression shapes, `fold()` and the EVL rules
|
|
13
|
+
* (EVL001 general, EVL003 for dynamic element-access keys) must agree —
|
|
14
|
+
* "EVL-flags <=> fold-rejects" — citing the same rule id and the same
|
|
15
|
+
* source position for every rejection.
|
|
16
|
+
*
|
|
17
|
+
* Every case here is a single-file, self-contained snippet (no imports, no
|
|
18
|
+
* unresolved cross-file symbols) so it stays inside the part of the subset
|
|
19
|
+
* that genuinely IS shared. A handful of real, environment/value-dependent
|
|
20
|
+
* exceptions are NOT unifiable through shape alone — see subset.ts's module
|
|
21
|
+
* doc for why — and are verified separately below, as documented
|
|
22
|
+
* divergences, rather than folded into the main equivalence loop.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface SubsetCase {
|
|
26
|
+
name: string;
|
|
27
|
+
/** The expression under test, plugged in as `new Thing({ x: <expr> })`. */
|
|
28
|
+
expr: string;
|
|
29
|
+
/** Extra top-level `const` declarations the expression may reference. */
|
|
30
|
+
preamble?: string;
|
|
31
|
+
intrinsics?: IntrinsicDef[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SUPPORTED_CASES: SubsetCase[] = [
|
|
35
|
+
{ name: "string literal", expr: `"x"` },
|
|
36
|
+
{ name: "numeric literal", expr: `42` },
|
|
37
|
+
{ name: "boolean literal", expr: `true` },
|
|
38
|
+
{ name: "null", expr: `null` },
|
|
39
|
+
{ name: "undefined", expr: `undefined` },
|
|
40
|
+
{ name: "template literal interpolating a const", preamble: `const name = "world";`, expr: "`hello-${name}`" },
|
|
41
|
+
{ name: "nested object literal with shorthand", preamble: `const c = 2;`, expr: `{ a: 1, b: { c } }` },
|
|
42
|
+
{ name: "object spread from a const object", preamble: `const base = { a: 1 };`, expr: `{ ...base, b: 2 }` },
|
|
43
|
+
{ name: "array spread from a const array", preamble: `const arr = [2, 3];`, expr: `[1, ...arr, 4]` },
|
|
44
|
+
{ name: "property access chain", preamble: `const config = { role: "arn" };`, expr: `config.role` },
|
|
45
|
+
{ name: "element access with a literal key", preamble: `const config = { role: "arn" };`, expr: `config["role"]` },
|
|
46
|
+
{ name: "logical-not unary", preamble: `const flag = true;`, expr: `!flag` },
|
|
47
|
+
{ name: "numeric negation unary", preamble: `const n = 5;`, expr: `-n` },
|
|
48
|
+
{ name: "whitelisted binary operator (+)", preamble: `const n = 5;`, expr: `n + 1` },
|
|
49
|
+
{ name: "whitelisted binary operator (===)", preamble: `const n = 5;`, expr: `n === 5` },
|
|
50
|
+
{ name: "conditional with both branches foldable", preamble: `const flag = true;`, expr: `flag ? "yes" : "no"` },
|
|
51
|
+
{ name: "as cast", expr: `(1 + 1) as number` },
|
|
52
|
+
{ name: "satisfies", expr: `"ok" satisfies string` },
|
|
53
|
+
{ name: "parenthesized", expr: `(("nested"))` },
|
|
54
|
+
{ name: "non-null assertion", preamble: `const n = 5;`, expr: `n!` },
|
|
55
|
+
{
|
|
56
|
+
name: "registered intrinsic tagged template with foldable interior",
|
|
57
|
+
preamble: `const name = "prefix";`,
|
|
58
|
+
expr: "Sub`${name}-data`",
|
|
59
|
+
intrinsics: [{ name: "Sub", isTag: true }],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const UNSUPPORTED_CASES: SubsetCase[] = [
|
|
64
|
+
{ name: "function call as a value", expr: `getName()` },
|
|
65
|
+
{ name: "method call as a value", expr: `config.getName()` },
|
|
66
|
+
{ name: "computed/dynamic object-literal key", expr: `{ [dynKey]: 1 }` },
|
|
67
|
+
{ name: "dynamic element-access key", expr: `config[dynKey]` },
|
|
68
|
+
{ name: "non-whitelisted binary operator (%)", expr: `n % 2` },
|
|
69
|
+
{ name: "non-whitelisted unary operator (~)", expr: `~n` },
|
|
70
|
+
{ name: "unsupported object member (accessor)", expr: `{ get y() { return 1; } }` },
|
|
71
|
+
{ name: "unsupported expression kind (arrow function)", expr: `() => 1` },
|
|
72
|
+
{ name: "template literal with an unfoldable interpolation", expr: "`pre-${getName()}`" },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
interface RunResult {
|
|
76
|
+
foldError: FoldError | undefined;
|
|
77
|
+
evl001Diags: ReturnType<typeof evl001NonLiteralExpressionRule.check>;
|
|
78
|
+
evl003Diags: ReturnType<typeof evl003DynamicPropertyAccessRule.check>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function run(c: SubsetCase): RunResult {
|
|
82
|
+
const source = `${c.preamble ?? ""}\nconst bad = new Thing({ x: ${c.expr} });`;
|
|
83
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
84
|
+
const consts = collectConsts(sourceFile);
|
|
85
|
+
const badInit = consts.get("bad");
|
|
86
|
+
if (!badInit || !ts.isNewExpression(badInit)) {
|
|
87
|
+
throw new Error(`fixture error: "bad" did not parse as a NewExpression in case "${c.name}"`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let foldError: FoldError | undefined;
|
|
91
|
+
try {
|
|
92
|
+
foldResource(badInit, consts, c.intrinsics ?? []);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e instanceof FoldError) {
|
|
95
|
+
foldError = e;
|
|
96
|
+
} else {
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
102
|
+
return {
|
|
103
|
+
foldError,
|
|
104
|
+
evl001Diags: evl001NonLiteralExpressionRule.check(context),
|
|
105
|
+
evl003Diags: evl003DynamicPropertyAccessRule.check(context),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe("subset equivalence — supported cases: fold succeeds AND EVL is clean", () => {
|
|
110
|
+
for (const c of SUPPORTED_CASES) {
|
|
111
|
+
test(c.name, () => {
|
|
112
|
+
const { foldError, evl001Diags, evl003Diags } = run(c);
|
|
113
|
+
expect(foldError, `fold() unexpectedly rejected: ${foldError?.message}`).toBeUndefined();
|
|
114
|
+
expect(evl001Diags, "EVL001 unexpectedly flagged a fold-supported construct").toHaveLength(0);
|
|
115
|
+
expect(evl003Diags, "EVL003 unexpectedly flagged a fold-supported construct").toHaveLength(0);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("subset equivalence — unsupported cases: fold rejects AND EVL flags the same rule id + position", () => {
|
|
121
|
+
for (const c of UNSUPPORTED_CASES) {
|
|
122
|
+
test(c.name, () => {
|
|
123
|
+
const { foldError, evl001Diags, evl003Diags } = run(c);
|
|
124
|
+
expect(foldError, "fold() unexpectedly accepted an unsupported construct").toBeInstanceOf(FoldError);
|
|
125
|
+
const err = foldError as FoldError;
|
|
126
|
+
|
|
127
|
+
const matching = err.ruleId === "EVL003" ? evl003Diags : evl001Diags;
|
|
128
|
+
expect(
|
|
129
|
+
matching.length,
|
|
130
|
+
`expected ${err.ruleId} to flag the same construct fold() rejected ("${err.message}")`,
|
|
131
|
+
).toBeGreaterThan(0);
|
|
132
|
+
expect(matching[0].ruleId).toBe(err.ruleId);
|
|
133
|
+
expect(matching[0].line, "EVL diagnostic line must match FoldError.line").toBe(err.line);
|
|
134
|
+
expect(matching[0].column, "EVL diagnostic column must match FoldError.column").toBe(err.column);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Documented, out-of-scope divergences (see subset.ts's module doc). These
|
|
141
|
+
* are NOT bugs introduced by #1024 — they're inherent to a syntax-only
|
|
142
|
+
* lint rule vs. an evaluator, and are asserted here so a future change that
|
|
143
|
+
* accidentally "fixes" (or silently regresses) one of them gets caught.
|
|
144
|
+
*/
|
|
145
|
+
describe("documented divergences — NOT unified by design (see subset.ts module doc)", () => {
|
|
146
|
+
test("identifier resolution: fold rejects an unresolved bare identifier; EVL001 does not (shape-only)", () => {
|
|
147
|
+
const source = `const bad = new Thing({ x: missingVar });`;
|
|
148
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
149
|
+
const consts = collectConsts(sourceFile);
|
|
150
|
+
const badInit = consts.get("bad") as ts.NewExpression;
|
|
151
|
+
|
|
152
|
+
expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
|
|
153
|
+
|
|
154
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
155
|
+
expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("intrinsic tag registration: fold rejects an unregistered tag; EVL001 does not (no lexicon manifest at lint time)", () => {
|
|
159
|
+
const source = "const bad = new Thing({ x: Unknown`plain` });";
|
|
160
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
161
|
+
const consts = collectConsts(sourceFile);
|
|
162
|
+
const badInit = consts.get("bad") as ts.NewExpression;
|
|
163
|
+
|
|
164
|
+
expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
|
|
165
|
+
|
|
166
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
167
|
+
expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("spread-source runtime type: fold rejects spreading a const number; no EVL rule catches it (needs evaluation)", () => {
|
|
171
|
+
const source = `
|
|
172
|
+
const n = 5;
|
|
173
|
+
const bad = new Thing({ x: { ...n } });
|
|
174
|
+
`;
|
|
175
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
176
|
+
const consts = collectConsts(sourceFile);
|
|
177
|
+
const badInit = consts.get("bad") as ts.NewExpression;
|
|
178
|
+
|
|
179
|
+
expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
|
|
180
|
+
|
|
181
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
182
|
+
expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
|
|
183
|
+
expect(evl004SpreadNonConstRule.check(context)).toHaveLength(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("&&/||/?? short-circuit: fold folds only the taken side; EVL is flow-insensitive and flags the untaken side too", () => {
|
|
187
|
+
// fold(): the left side is falsy, so `sideEffect()` is never folded — succeeds.
|
|
188
|
+
const shortCircuitSrc = ts.createSourceFile(
|
|
189
|
+
"t.ts",
|
|
190
|
+
`const x = false && sideEffect();`,
|
|
191
|
+
ts.ScriptTarget.Latest,
|
|
192
|
+
true,
|
|
193
|
+
);
|
|
194
|
+
const shortCircuitConsts = collectConsts(shortCircuitSrc);
|
|
195
|
+
const foldedValue = fold(shortCircuitConsts.get("x") as ts.Expression, shortCircuitConsts);
|
|
196
|
+
expect(foldedValue).toBe(false);
|
|
197
|
+
|
|
198
|
+
// EVL001 has no evaluator — it requires every operand to be shape-valid,
|
|
199
|
+
// so it flags the untaken `sideEffect()` branch even though fold()
|
|
200
|
+
// never touches it.
|
|
201
|
+
const source = `const bad = new Thing({ x: false && sideEffect() });`;
|
|
202
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
203
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
204
|
+
expect(evl001NonLiteralExpressionRule.check(context).length).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("tagged-template interior: fold rejects an unfoldable interpolation in a registered intrinsic tag; EVL is lenient (no registry, interiors opaque)", () => {
|
|
208
|
+
// `Sub`${getName()}`` — fold, with the intrinsic registry, folds the Sub
|
|
209
|
+
// tag's interior and rejects the getName() call. EVL has no registry and
|
|
210
|
+
// can't tell an intrinsic call (Ref, legit) from a plain one, so it treats
|
|
211
|
+
// tagged-template interiors as opaque and does not flag — otherwise it would
|
|
212
|
+
// false-flag `Sub`${Ref(env)}`` and break every intrinsic-using example.
|
|
213
|
+
const source = "const bad = new Thing({ x: Sub`${getName()}` });";
|
|
214
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
215
|
+
const consts = collectConsts(sourceFile);
|
|
216
|
+
const badInit = consts.get("bad") as ts.NewExpression;
|
|
217
|
+
|
|
218
|
+
expect(() => foldResource(badInit, consts, [{ name: "Sub", isTag: true }])).toThrow(FoldError);
|
|
219
|
+
|
|
220
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
221
|
+
expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("nested resource construction: fold rejects a nested `new Type()` used as a value (falls back to run); EVL001 allows it statically", () => {
|
|
225
|
+
// A top-level `new Type()` folds (fold-import constructs a real Declarable),
|
|
226
|
+
// but a NESTED one as a property value can only fold to a {__resource,props}
|
|
227
|
+
// envelope that is never constructed, so it would serialize wrong (real
|
|
228
|
+
// fold-vs-run drift, caught by the #1025 differential on gitlab). fold()
|
|
229
|
+
// therefore rejects it, falling the file back to run; EVL allows it (a valid
|
|
230
|
+
// TS constructor call), so this is an inherent, out-of-scope divergence.
|
|
231
|
+
const source = `const bad = new Thing({ x: new Inner({ y: 1 }) });`;
|
|
232
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
233
|
+
const consts = collectConsts(sourceFile);
|
|
234
|
+
const badInit = consts.get("bad") as ts.NewExpression;
|
|
235
|
+
|
|
236
|
+
expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
|
|
237
|
+
|
|
238
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
239
|
+
expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
|
|
240
|
+
});
|
|
241
|
+
});
|