@intentius/chant 0.68.1 → 0.69.1
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/discovery/import.d.ts +15 -1
- package/dist/discovery/import.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +9 -3
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/lint/rules/evl011-symbolic-in-template.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/discovery/import.test.ts +110 -2
- package/src/discovery/import.ts +50 -2
- package/src/fold/fold.test.ts +23 -0
- package/src/fold/fold.ts +18 -3
- package/src/lint/rules/evl011-symbolic-in-template.test.ts +28 -0
- package/src/lint/rules/evl011-symbolic-in-template.ts +32 -9
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forget every recorded evaluation failure.
|
|
3
|
+
*
|
|
4
|
+
* For a harness that deliberately starts a fresh module graph and wants the
|
|
5
|
+
* failures forgotten with it. Nothing in chant calls this: a real build
|
|
6
|
+
* imports once, and a second import inside one process is supposed to see what
|
|
7
|
+
* the first one saw.
|
|
8
|
+
*
|
|
9
|
+
* It does not make a module re-evaluate. Node's own registry still holds the
|
|
10
|
+
* failed record and still re-throws; this only clears chant's memory of it. A
|
|
11
|
+
* caller wanting genuine re-evaluation needs a fresh process.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resetImportFailures(): void;
|
|
1
14
|
/**
|
|
2
15
|
* Dynamically import a module and return its exports.
|
|
3
16
|
*
|
|
4
17
|
* @param path - The file path to import
|
|
5
18
|
* @returns The module exports
|
|
6
|
-
* @throws {DiscoveryError} with type "import" if the import fails
|
|
19
|
+
* @throws {DiscoveryError} with type "import" if the import fails, including
|
|
20
|
+
* on every later import of a module whose evaluation has already failed
|
|
7
21
|
*/
|
|
8
22
|
export declare function importModule(path: string): Promise<Record<string, unknown>>;
|
|
9
23
|
//# sourceMappingURL=import.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/discovery/import.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/discovery/import.ts"],"names":[],"mappings":"AA4BA;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAalC"}
|
package/dist/fold/fold.d.ts
CHANGED
|
@@ -126,9 +126,15 @@ export type FoldedValue = string | number | boolean | null | undefined | FoldedV
|
|
|
126
126
|
* `undefined` for a value that has an honest string form.
|
|
127
127
|
*
|
|
128
128
|
* These are the shapes `fold()` produces for things the build resolves later:
|
|
129
|
-
* a resource attribute, a lexicon intrinsic in either of its two forms,
|
|
130
|
-
* registered authoring helper
|
|
131
|
-
*
|
|
129
|
+
* a resource attribute, a lexicon intrinsic in either of its two forms, a
|
|
130
|
+
* registered authoring helper, a nested construction used as a value, and a
|
|
131
|
+
* composite `.step`. None of them can be concatenated into a string here,
|
|
132
|
+
* because what they stand for is not known until the build runs.
|
|
133
|
+
*
|
|
134
|
+
* Five of the six envelope kinds. `__symbol` is the sixth and is deliberately
|
|
135
|
+
* absent: it is produced only inside an intrinsic's interior and cannot reach
|
|
136
|
+
* a plain template span, so a case for it would be unreachable code claiming
|
|
137
|
+
* to guard something (#2397).
|
|
132
138
|
*/
|
|
133
139
|
export declare function symbolicEnvelopeKind(value: unknown): string | undefined;
|
|
134
140
|
export interface AttrRefValue {
|
package/dist/fold/fold.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fold.d.ts","sourceRoot":"","sources":["../../src/fold/fold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAAoE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACjH,OAAO,EAaL,KAAK,YAAY,EAClB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkGG;AAEH;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,GACT,WAAW,EAAE,GACb;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAA;CAAE,GAC9B,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,uBAAuB,CAAC;AAE5B;;;;;;;;;;;;GAYG;AACH
|
|
1
|
+
{"version":3,"file":"fold.d.ts","sourceRoot":"","sources":["../../src/fold/fold.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evl011-symbolic-in-template.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/evl011-symbolic-in-template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"evl011-symbolic-in-template.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/evl011-symbolic-in-template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,SAAS,CAAC;AA+GrE,eAAO,MAAM,4BAA4B,EAAE,QAU1C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { importModule } from "./import";
|
|
2
|
+
import { importModule, resetImportFailures } from "./import";
|
|
3
|
+
import { discover } from "./index";
|
|
3
4
|
import { DiscoveryError } from "../errors";
|
|
4
5
|
import { withTestDir, expectToThrow } from "@intentius/chant-test-utils";
|
|
5
6
|
import { writeFile } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
7
8
|
|
|
8
9
|
describe("importModule", () => {
|
|
9
10
|
|
|
@@ -198,3 +199,110 @@ describe("importModule", () => {
|
|
|
198
199
|
});
|
|
199
200
|
});
|
|
200
201
|
});
|
|
202
|
+
|
|
203
|
+
describe("a module whose evaluation threw stays thrown (#2368)", () => {
|
|
204
|
+
/**
|
|
205
|
+
* The spec records an evaluation failure on the module record and re-throws
|
|
206
|
+
* it on every later import, forever. Node does that; vitest's module runner
|
|
207
|
+
* does not, and hands the second importer a namespace holding whatever was
|
|
208
|
+
* initialized before the throw.
|
|
209
|
+
*
|
|
210
|
+
* These assert the contract directly. They do not reproduce the runner bug —
|
|
211
|
+
* a module written to a temp directory is outside vitest's transform root and
|
|
212
|
+
* is imported by Node, which already records the failure. The reproduction is
|
|
213
|
+
* the corpus-backed test at the bottom of this file, which is the one that
|
|
214
|
+
* fails without the failure map.
|
|
215
|
+
*/
|
|
216
|
+
test("a second import throws the same error, not a half-initialized namespace", async () => {
|
|
217
|
+
await withTestDir(async (testDir) => {
|
|
218
|
+
const filePath = join(testDir, "throws.ts");
|
|
219
|
+
await writeFile(filePath, `const o = undefined as unknown as { k: string };\nexport const value = o.k;\n`);
|
|
220
|
+
|
|
221
|
+
const first = await expectToThrow(() => importModule(filePath), DiscoveryError);
|
|
222
|
+
const second = await expectToThrow(() => importModule(filePath), DiscoveryError);
|
|
223
|
+
|
|
224
|
+
expect(second.message).toBe(first.message);
|
|
225
|
+
expect(second.file).toBe(filePath);
|
|
226
|
+
// The same error object, because it is the recorded one being replayed
|
|
227
|
+
// rather than a second failure that happened to look alike.
|
|
228
|
+
expect(second).toBe(first);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a third and fourth import keep throwing", async () => {
|
|
233
|
+
await withTestDir(async (testDir) => {
|
|
234
|
+
const filePath = join(testDir, "throws-again.ts");
|
|
235
|
+
await writeFile(filePath, `const o = undefined as unknown as { k: string };\nexport const value = o.k;\n`);
|
|
236
|
+
|
|
237
|
+
await expectToThrow(() => importModule(filePath), DiscoveryError);
|
|
238
|
+
await expectToThrow(() => importModule(filePath), DiscoveryError);
|
|
239
|
+
await expectToThrow(() => importModule(filePath), DiscoveryError);
|
|
240
|
+
await expectToThrow(() => importModule(filePath), DiscoveryError);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("a module that imports cleanly is unaffected and is not cached as failed", async () => {
|
|
245
|
+
await withTestDir(async (testDir) => {
|
|
246
|
+
const filePath = join(testDir, "fine.ts");
|
|
247
|
+
await writeFile(filePath, `export const value = "ok";\n`);
|
|
248
|
+
|
|
249
|
+
const a = await importModule(filePath);
|
|
250
|
+
const b = await importModule(filePath);
|
|
251
|
+
expect(a.value).toBe("ok");
|
|
252
|
+
expect(b.value).toBe("ok");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("resetImportFailures forgets the record, and does not make the module re-evaluate", async () => {
|
|
257
|
+
await withTestDir(async (testDir) => {
|
|
258
|
+
const filePath = join(testDir, "throws-reset.ts");
|
|
259
|
+
await writeFile(filePath, `const o = undefined as unknown as { k: string };\nexport const value = o.k;\n`);
|
|
260
|
+
|
|
261
|
+
const first = await expectToThrow(() => importModule(filePath), DiscoveryError);
|
|
262
|
+
resetImportFailures();
|
|
263
|
+
|
|
264
|
+
// Under Node the registry still holds the failed record and throws
|
|
265
|
+
// again; under vitest the module is served from its cache. Either way
|
|
266
|
+
// the escape hatch only clears chant's memory, which is what its doc
|
|
267
|
+
// says — so this asserts the clearing, not a re-evaluation.
|
|
268
|
+
let replayedSameObject = false;
|
|
269
|
+
try {
|
|
270
|
+
await importModule(filePath);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
replayedSameObject = err === first;
|
|
273
|
+
}
|
|
274
|
+
expect(replayedSameObject).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("discover() reports the same errors on a second pass (#2368)", () => {
|
|
280
|
+
/**
|
|
281
|
+
* The reproduction from the issue, over the corpus entry that exposed it.
|
|
282
|
+
*
|
|
283
|
+
* It has to be a file inside the project: vitest transforms and caches those,
|
|
284
|
+
* and it is that cache which forgets an evaluation failure. A module written
|
|
285
|
+
* to a temp directory outside the runner's root is imported by Node natively
|
|
286
|
+
* and already behaves correctly, so a fixture built with `withTestDir` would
|
|
287
|
+
* pass with or without the fix and prove nothing.
|
|
288
|
+
*
|
|
289
|
+
* `examples/fold-adversarial/src/nullish-property-read.ts` folds to a refusal
|
|
290
|
+
* and then throws a TypeError when the run path imports it (#2328). Before
|
|
291
|
+
* the failure map: `discover #1 errors=1`, `discover #2 errors=0`.
|
|
292
|
+
*/
|
|
293
|
+
const ADVERSARIAL = resolve(import.meta.dirname, "../../../../examples/fold-adversarial/src");
|
|
294
|
+
|
|
295
|
+
test("the corpus entry with a throwing file reports it on every pass", async () => {
|
|
296
|
+
const first = await discover(ADVERSARIAL);
|
|
297
|
+
const second = await discover(ADVERSARIAL);
|
|
298
|
+
const third = await discover(ADVERSARIAL);
|
|
299
|
+
|
|
300
|
+
expect(first.errors.length).toBeGreaterThan(0);
|
|
301
|
+
expect(second.errors.length).toBe(first.errors.length);
|
|
302
|
+
expect(third.errors.length).toBe(first.errors.length);
|
|
303
|
+
|
|
304
|
+
const named = (r: { errors: unknown[] }): boolean =>
|
|
305
|
+
(r.errors[0] as { file?: string }).file?.endsWith("nullish-property-read.ts") === true;
|
|
306
|
+
expect(named(first) && named(second) && named(third)).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
});
|
package/src/discovery/import.ts
CHANGED
|
@@ -1,20 +1,68 @@
|
|
|
1
1
|
import { DiscoveryError } from "../errors";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Modules whose evaluation threw, and the error each threw with (#2368).
|
|
5
|
+
*
|
|
6
|
+
* The ES module spec says an evaluation failure is recorded on the module
|
|
7
|
+
* record and re-thrown on every later import of the same specifier, forever,
|
|
8
|
+
* in that realm. Node implements it — including after the file is edited, so a
|
|
9
|
+
* module that threw keeps throwing until the process restarts.
|
|
10
|
+
*
|
|
11
|
+
* Vitest's module runner does not. It caches the module as *evaluated* and
|
|
12
|
+
* hands later importers a namespace carrying whichever bindings were
|
|
13
|
+
* initialized before the throw, so the second import resolves with
|
|
14
|
+
* half-initialized exports and no error at all.
|
|
15
|
+
*
|
|
16
|
+
* That divergence is invisible to `chant build`, which imports a directory
|
|
17
|
+
* once per process, and highly visible to anything that builds the same
|
|
18
|
+
* directory twice and compares: whichever build ran first carried the error
|
|
19
|
+
* and every build after it reported none, so error parity came out of the
|
|
20
|
+
* ordering rather than out of the tool.
|
|
21
|
+
*
|
|
22
|
+
* This map is what makes the answer the same either way. Under Node it never
|
|
23
|
+
* fires — `import()` throws again before the lookup matters — so it costs
|
|
24
|
+
* nothing and changes nothing there. Under a runner that forgets, it supplies
|
|
25
|
+
* the memory the spec requires.
|
|
26
|
+
*/
|
|
27
|
+
const evaluationFailures = new Map<string, DiscoveryError>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Forget every recorded evaluation failure.
|
|
31
|
+
*
|
|
32
|
+
* For a harness that deliberately starts a fresh module graph and wants the
|
|
33
|
+
* failures forgotten with it. Nothing in chant calls this: a real build
|
|
34
|
+
* imports once, and a second import inside one process is supposed to see what
|
|
35
|
+
* the first one saw.
|
|
36
|
+
*
|
|
37
|
+
* It does not make a module re-evaluate. Node's own registry still holds the
|
|
38
|
+
* failed record and still re-throws; this only clears chant's memory of it. A
|
|
39
|
+
* caller wanting genuine re-evaluation needs a fresh process.
|
|
40
|
+
*/
|
|
41
|
+
export function resetImportFailures(): void {
|
|
42
|
+
evaluationFailures.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
3
45
|
/**
|
|
4
46
|
* Dynamically import a module and return its exports.
|
|
5
47
|
*
|
|
6
48
|
* @param path - The file path to import
|
|
7
49
|
* @returns The module exports
|
|
8
|
-
* @throws {DiscoveryError} with type "import" if the import fails
|
|
50
|
+
* @throws {DiscoveryError} with type "import" if the import fails, including
|
|
51
|
+
* on every later import of a module whose evaluation has already failed
|
|
9
52
|
*/
|
|
10
53
|
export async function importModule(
|
|
11
54
|
path: string
|
|
12
55
|
): Promise<Record<string, unknown>> {
|
|
56
|
+
const failed = evaluationFailures.get(path);
|
|
57
|
+
if (failed) throw failed;
|
|
58
|
+
|
|
13
59
|
try {
|
|
14
60
|
return await import(path);
|
|
15
61
|
} catch (error) {
|
|
16
62
|
const message =
|
|
17
63
|
error instanceof Error ? error.message : "Unknown import error";
|
|
18
|
-
|
|
64
|
+
const discoveryError = new DiscoveryError(path, message, "import");
|
|
65
|
+
evaluationFailures.set(path, discoveryError);
|
|
66
|
+
throw discoveryError;
|
|
19
67
|
}
|
|
20
68
|
}
|
package/src/fold/fold.test.ts
CHANGED
|
@@ -100,6 +100,29 @@ describe("fold — a symbolic value in a plain template (#2349)", () => {
|
|
|
100
100
|
expect(() => "prefix" + (bucket.arn as unknown as string)).toThrow(/no string form/);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
|
|
104
|
+
test("a nested construction used as a value is refused too (#2397)", () => {
|
|
105
|
+
const src = `
|
|
106
|
+
const x = \`\${new S3Bucket({ BucketName: "b" })}-tag\`;
|
|
107
|
+
`;
|
|
108
|
+
expect(() => foldConst(src, "x")).toThrow(FoldError);
|
|
109
|
+
try {
|
|
110
|
+
foldConst(src, "x");
|
|
111
|
+
} catch (e) {
|
|
112
|
+
expect((e as FoldError).message).toContain("new S3Bucket");
|
|
113
|
+
expect((e as FoldError).message).toContain("[object Object]");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a composite `.step` is refused too (#2397)", () => {
|
|
118
|
+
// The run path builds a real instance whose default toString is the same
|
|
119
|
+
// "[object Object]", so this agreed across paths exactly as #2349 did.
|
|
120
|
+
const src = `
|
|
121
|
+
const x = \`\${Checkout({}).step}\`;
|
|
122
|
+
`;
|
|
123
|
+
expect(() => foldConst(src, "x")).toThrow(FoldError);
|
|
124
|
+
});
|
|
125
|
+
|
|
103
126
|
test("an ordinary value in a template still interpolates", () => {
|
|
104
127
|
const src = `
|
|
105
128
|
const region = "us-east-1";
|
package/src/fold/fold.ts
CHANGED
|
@@ -156,9 +156,15 @@ export type FoldedValue =
|
|
|
156
156
|
* `undefined` for a value that has an honest string form.
|
|
157
157
|
*
|
|
158
158
|
* These are the shapes `fold()` produces for things the build resolves later:
|
|
159
|
-
* a resource attribute, a lexicon intrinsic in either of its two forms,
|
|
160
|
-
* registered authoring helper
|
|
161
|
-
*
|
|
159
|
+
* a resource attribute, a lexicon intrinsic in either of its two forms, a
|
|
160
|
+
* registered authoring helper, a nested construction used as a value, and a
|
|
161
|
+
* composite `.step`. None of them can be concatenated into a string here,
|
|
162
|
+
* because what they stand for is not known until the build runs.
|
|
163
|
+
*
|
|
164
|
+
* Five of the six envelope kinds. `__symbol` is the sixth and is deliberately
|
|
165
|
+
* absent: it is produced only inside an intrinsic's interior and cannot reach
|
|
166
|
+
* a plain template span, so a case for it would be unreachable code claiming
|
|
167
|
+
* to guard something (#2397).
|
|
162
168
|
*/
|
|
163
169
|
export function symbolicEnvelopeKind(value: unknown): string | undefined {
|
|
164
170
|
if (typeof value !== "object" || value === null) return undefined;
|
|
@@ -173,6 +179,15 @@ export function symbolicEnvelopeKind(value: unknown): string | undefined {
|
|
|
173
179
|
}
|
|
174
180
|
if ("__intrinsic" in v && typeof v.__intrinsic === "string") return `the intrinsic \`${v.__intrinsic}\``;
|
|
175
181
|
if ("__helper" in v && typeof v.__helper === "string") return `the helper \`${v.__helper}\``;
|
|
182
|
+
// A nested construction used as a value (#1169). The run path builds a real
|
|
183
|
+
// instance here, whose default `toString` is `"[object Object]"` too, so
|
|
184
|
+
// fold and run agreed and the differential stayed silent — the same shape
|
|
185
|
+
// as #2349 itself (#2397).
|
|
186
|
+
if ("__resource" in v && typeof v.__resource === "string") return `a nested \`new ${v.__resource}\``;
|
|
187
|
+
// The `.step` idiom (#1174).
|
|
188
|
+
if ("__compositeStep" in v && typeof v.__compositeStep === "string") {
|
|
189
|
+
return `the composite step \`${v.__compositeStep}(…).step\``;
|
|
190
|
+
}
|
|
176
191
|
return undefined;
|
|
177
192
|
}
|
|
178
193
|
|
|
@@ -65,6 +65,34 @@ describe("EVL011: a symbolic reference in a plain template", () => {
|
|
|
65
65
|
expect(evl011SymbolicInTemplateRule.check(ctx)).toHaveLength(0);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
|
|
69
|
+
test("flags a nested construction used as a value (#2397)", () => {
|
|
70
|
+
const ctx = createContext(`
|
|
71
|
+
const x = \`\${new Image({ name: "n" })}-tag\`;
|
|
72
|
+
`);
|
|
73
|
+
const found = evl011SymbolicInTemplateRule.check(ctx);
|
|
74
|
+
expect(found).toHaveLength(1);
|
|
75
|
+
expect(found[0].message).toContain("new Image");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("flags a composite `.step` (#2397)", () => {
|
|
79
|
+
const ctx = createContext(`
|
|
80
|
+
const x = \`\${Checkout({}).step}\`;
|
|
81
|
+
`);
|
|
82
|
+
const found = evl011SymbolicInTemplateRule.check(ctx);
|
|
83
|
+
expect(found).toHaveLength(1);
|
|
84
|
+
expect(found[0].message).toContain("Checkout(…).step");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("an ordinary `.step`-less property on a call is not flagged", () => {
|
|
88
|
+
// The false positive worth avoiding: `.step` is the idiom, and a rule
|
|
89
|
+
// cannot know which callees are composites, so only that name qualifies.
|
|
90
|
+
const ctx = createContext(`
|
|
91
|
+
const x = \`\${getConfig().region}\`;
|
|
92
|
+
`);
|
|
93
|
+
expect(evl011SymbolicInTemplateRule.check(ctx)).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
68
96
|
test("says nothing about a reference outside a template", () => {
|
|
69
97
|
const ctx = createContext(`
|
|
70
98
|
const bucket = new S3Bucket({ name: "b" });
|
|
@@ -49,6 +49,33 @@ function resourceBindings(sourceFile: ts.SourceFile): Set<string> {
|
|
|
49
49
|
return names;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* What this span is, if it is one of the shapes that has no string form — or
|
|
54
|
+
* `undefined` for an ordinary expression.
|
|
55
|
+
*
|
|
56
|
+
* The three kinds a syntactic rule can see, matching what `fold()` refuses
|
|
57
|
+
* (#2397): a resource attribute on an identifier bound by `new` in this file,
|
|
58
|
+
* a nested construction used as a value, and a composite `.step`.
|
|
59
|
+
*/
|
|
60
|
+
function symbolicSpan(expr: ts.Expression, bound: Set<string>): string | undefined {
|
|
61
|
+
if (ts.isNewExpression(expr)) {
|
|
62
|
+
const name = ts.isIdentifier(expr.expression) ? expr.expression.text : "…";
|
|
63
|
+
return `a nested \`new ${name}\``;
|
|
64
|
+
}
|
|
65
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
66
|
+
// `Checkout({}).step` — a call's `.step`, whatever the callee is: the
|
|
67
|
+
// idiom is the shape, and a rule cannot know which callees are composites.
|
|
68
|
+
if (expr.name.text === "step" && ts.isCallExpression(expr.expression)) {
|
|
69
|
+
const callee = ts.isIdentifier(expr.expression.expression) ? expr.expression.expression.text : "…";
|
|
70
|
+
return `the composite step \`${callee}(…).step\``;
|
|
71
|
+
}
|
|
72
|
+
if (ts.isIdentifier(expr.expression) && bound.has(expr.expression.text)) {
|
|
73
|
+
return `\`${expr.expression.text}.${expr.name.text}\` is a resource attribute and`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
function checkNode(
|
|
53
80
|
node: ts.Node,
|
|
54
81
|
context: LintContext,
|
|
@@ -60,11 +87,8 @@ function checkNode(
|
|
|
60
87
|
if (ts.isTemplateExpression(node) && !ts.isTaggedTemplateExpression(node.parent)) {
|
|
61
88
|
for (const span of node.templateSpans) {
|
|
62
89
|
const expr = span.expression;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
ts.isIdentifier(expr.expression) &&
|
|
66
|
-
bound.has(expr.expression.text)
|
|
67
|
-
) {
|
|
90
|
+
const what = symbolicSpan(expr, bound);
|
|
91
|
+
if (what) {
|
|
68
92
|
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
69
93
|
expr.getStart(context.sourceFile),
|
|
70
94
|
);
|
|
@@ -75,10 +99,9 @@ function checkNode(
|
|
|
75
99
|
ruleId: "EVL011",
|
|
76
100
|
severity: "error",
|
|
77
101
|
message:
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"(`Sub`${…}`` for CloudFormation), or move the reference out of the template.",
|
|
102
|
+
`${what} has no string form until the build resolves it — a plain template would ` +
|
|
103
|
+
"stringify it as \"[object Object]\". Use the lexicon's own intrinsic, whose interior " +
|
|
104
|
+
"handles these (`Sub`${…}`` for CloudFormation), or move it out of the template.",
|
|
82
105
|
});
|
|
83
106
|
}
|
|
84
107
|
}
|