@intentius/chant-lexicon-aws 0.0.6 → 0.0.9
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/integrity.json +25 -10
- package/dist/manifest.json +1 -1
- package/dist/meta.json +9444 -4597
- package/dist/rules/cf-refs.ts +99 -0
- package/dist/rules/ext001.ts +32 -25
- package/dist/rules/hardcoded-region.ts +1 -0
- package/dist/rules/iam-wildcard.ts +1 -0
- package/dist/rules/s3-encryption.ts +3 -3
- package/dist/rules/waw016.ts +86 -0
- package/dist/rules/waw017.ts +53 -0
- package/dist/rules/waw018.ts +71 -0
- package/dist/rules/waw019.ts +82 -0
- package/dist/rules/waw020.ts +64 -0
- package/dist/rules/waw021.ts +53 -0
- package/dist/rules/waw022.ts +43 -0
- package/dist/rules/waw023.ts +47 -0
- package/dist/rules/waw024.ts +54 -0
- package/dist/rules/waw025.ts +43 -0
- package/dist/rules/waw026.ts +46 -0
- package/dist/rules/waw027.ts +50 -0
- package/dist/rules/waw028.ts +47 -0
- package/dist/rules/waw029.ts +62 -0
- package/dist/rules/waw030.ts +246 -0
- package/dist/skills/chant-aws.md +430 -0
- package/dist/types/index.d.ts +58525 -58501
- package/package.json +2 -2
- package/src/actions/actions.test.ts +75 -0
- package/src/actions/dynamodb.ts +36 -0
- package/src/actions/ecr.ts +9 -0
- package/src/actions/ecs.ts +5 -0
- package/src/actions/iam.ts +3 -0
- package/src/actions/index.ts +9 -0
- package/src/actions/lambda.ts +11 -0
- package/src/actions/logs.ts +4 -0
- package/src/actions/s3.ts +34 -0
- package/src/actions/sns.ts +5 -0
- package/src/actions/sqs.ts +15 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +20 -20
- package/src/codegen/docs-links.test.ts +143 -0
- package/src/codegen/docs.ts +294 -124
- package/src/codegen/generate-lexicon.ts +8 -0
- package/src/codegen/generate-typescript.ts +25 -1
- package/src/codegen/generate.ts +1 -13
- package/src/codegen/package.ts +2 -0
- package/src/codegen/typecheck.test.ts +1 -1
- package/src/composites/composites.test.ts +442 -0
- package/src/composites/fargate-alb.ts +253 -0
- package/src/composites/index.ts +20 -0
- package/src/composites/lambda-api.ts +20 -0
- package/src/composites/lambda-dynamodb.ts +64 -0
- package/src/composites/lambda-eventbridge.ts +36 -0
- package/src/composites/lambda-function.ts +76 -0
- package/src/composites/lambda-s3.ts +72 -0
- package/src/composites/lambda-sns.ts +30 -0
- package/src/composites/lambda-sqs.ts +44 -0
- package/src/composites/scheduled-lambda.ts +37 -0
- package/src/composites/vpc-default.ts +148 -0
- package/src/default-tags.test.ts +38 -0
- package/src/default-tags.ts +77 -0
- package/src/generated/index.d.ts +58525 -58501
- package/src/generated/index.ts +1351 -1351
- package/src/generated/lexicon-aws.json +9444 -4597
- package/src/import/generator.test.ts +5 -5
- package/src/import/generator.ts +4 -4
- package/src/import/roundtrip-fixtures.test.ts +2 -1
- package/src/import/roundtrip.test.ts +5 -5
- package/src/index.ts +21 -0
- package/src/integration.test.ts +92 -21
- package/src/intrinsics.ts +24 -13
- package/src/lint/post-synth/cf-refs.ts +99 -0
- package/src/lint/post-synth/ext001.test.ts +214 -31
- package/src/lint/post-synth/ext001.ts +32 -25
- package/src/lint/post-synth/waw013.test.ts +120 -0
- package/src/lint/post-synth/waw014.test.ts +121 -0
- package/src/lint/post-synth/waw015.test.ts +147 -0
- package/src/lint/post-synth/waw016.test.ts +141 -0
- package/src/lint/post-synth/waw016.ts +86 -0
- package/src/lint/post-synth/waw017.test.ts +130 -0
- package/src/lint/post-synth/waw017.ts +53 -0
- package/src/lint/post-synth/waw018.test.ts +109 -0
- package/src/lint/post-synth/waw018.ts +71 -0
- package/src/lint/post-synth/waw019.test.ts +138 -0
- package/src/lint/post-synth/waw019.ts +82 -0
- package/src/lint/post-synth/waw020.test.ts +125 -0
- package/src/lint/post-synth/waw020.ts +64 -0
- package/src/lint/post-synth/waw021.test.ts +81 -0
- package/src/lint/post-synth/waw021.ts +53 -0
- package/src/lint/post-synth/waw022.test.ts +54 -0
- package/src/lint/post-synth/waw022.ts +43 -0
- package/src/lint/post-synth/waw023.test.ts +53 -0
- package/src/lint/post-synth/waw023.ts +47 -0
- package/src/lint/post-synth/waw024.test.ts +64 -0
- package/src/lint/post-synth/waw024.ts +54 -0
- package/src/lint/post-synth/waw025.test.ts +42 -0
- package/src/lint/post-synth/waw025.ts +43 -0
- package/src/lint/post-synth/waw026.test.ts +54 -0
- package/src/lint/post-synth/waw026.ts +46 -0
- package/src/lint/post-synth/waw027.test.ts +63 -0
- package/src/lint/post-synth/waw027.ts +50 -0
- package/src/lint/post-synth/waw028.test.ts +68 -0
- package/src/lint/post-synth/waw028.ts +47 -0
- package/src/lint/post-synth/waw029.test.ts +179 -0
- package/src/lint/post-synth/waw029.ts +62 -0
- package/src/lint/post-synth/waw030.test.ts +800 -0
- package/src/lint/post-synth/waw030.ts +246 -0
- package/src/lint/rules/hardcoded-region.ts +1 -0
- package/src/lint/rules/iam-wildcard.ts +1 -0
- package/src/lint/rules/rules.test.ts +8 -8
- package/src/lint/rules/s3-encryption.ts +3 -3
- package/src/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +17 -0
- package/src/nested-stack-integration.test.ts +100 -0
- package/src/nested-stack.ts +2 -2
- package/src/plugin.test.ts +13 -15
- package/src/plugin.ts +552 -114
- package/src/serializer.test.ts +370 -43
- package/src/serializer.ts +69 -17
- package/src/spec/fetch.ts +10 -0
- package/src/spec/parse.test.ts +141 -0
- package/src/spec/parse.ts +40 -0
- package/src/taggable.ts +44 -0
- package/src/testdata/nested-stacks/app.ts +26 -0
- package/src/testdata/nested-stacks/network/outputs.ts +17 -0
- package/src/testdata/nested-stacks/network/security.ts +17 -0
- package/src/testdata/nested-stacks/network/vpc.ts +54 -0
- package/dist/skills/aws-cloudformation.md +0 -41
- package/src/codegen/rollback.test.ts +0 -80
- package/src/codegen/rollback.ts +0 -20
|
@@ -1,68 +1,251 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
2
|
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
4
|
-
import { ext001 } from "./ext001";
|
|
3
|
+
import { ext001, checkExtensionConstraints, type ExtensionConstraint } from "./ext001";
|
|
5
4
|
|
|
6
5
|
function makeCtx(template: object) {
|
|
7
6
|
return createPostSynthContext({ aws: template });
|
|
8
7
|
}
|
|
9
8
|
|
|
9
|
+
/** Synthetic constraint map — no disk dependency. */
|
|
10
|
+
function fakeConstraints(): Map<string, ExtensionConstraint[]> {
|
|
11
|
+
return new Map([
|
|
12
|
+
["AWS::EC2::Instance", [
|
|
13
|
+
{
|
|
14
|
+
name: "SubnetRequired",
|
|
15
|
+
type: "if_then",
|
|
16
|
+
condition: { required: ["InstanceType"] },
|
|
17
|
+
requirement: { required: ["SubnetId"] },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "SpotExcludesPlacement",
|
|
21
|
+
type: "dependent_excluded",
|
|
22
|
+
requirement: { InstanceMarketOptions: ["PlacementGroup"] },
|
|
23
|
+
},
|
|
24
|
+
]],
|
|
25
|
+
["AWS::ECS::Service", [
|
|
26
|
+
{
|
|
27
|
+
name: "LaunchTypeOrCapacity",
|
|
28
|
+
type: "required_xor",
|
|
29
|
+
requirement: ["LaunchType", "CapacityProviderStrategy"],
|
|
30
|
+
},
|
|
31
|
+
]],
|
|
32
|
+
["AWS::S3::Bucket", [
|
|
33
|
+
{
|
|
34
|
+
name: "EncryptionOrNotification",
|
|
35
|
+
type: "required_or",
|
|
36
|
+
requirement: ["BucketEncryption", "NotificationConfiguration"],
|
|
37
|
+
},
|
|
38
|
+
]],
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
describe("EXT001: Extension Constraint Violation", () => {
|
|
11
43
|
test("check metadata", () => {
|
|
12
44
|
expect(ext001.id).toBe("EXT001");
|
|
13
45
|
expect(ext001.description).toContain("constraint");
|
|
14
46
|
});
|
|
15
47
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
48
|
+
// --- if_then ---
|
|
49
|
+
|
|
50
|
+
test("if_then: flags when condition matches and requirement missing", () => {
|
|
51
|
+
const ctx = makeCtx({
|
|
52
|
+
Resources: {
|
|
53
|
+
MyInstance: {
|
|
54
|
+
Type: "AWS::EC2::Instance",
|
|
55
|
+
Properties: { InstanceType: "t3.micro" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
60
|
+
expect(diags).toHaveLength(1);
|
|
61
|
+
expect(diags[0].checkId).toBe("EXT001");
|
|
62
|
+
expect(diags[0].severity).toBe("error");
|
|
63
|
+
expect(diags[0].message).toContain("SubnetRequired");
|
|
64
|
+
expect(diags[0].message).toContain("SubnetId");
|
|
65
|
+
expect(diags[0].entity).toBe("MyInstance");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("if_then: no diagnostic when requirement satisfied", () => {
|
|
69
|
+
const ctx = makeCtx({
|
|
70
|
+
Resources: {
|
|
71
|
+
MyInstance: {
|
|
72
|
+
Type: "AWS::EC2::Instance",
|
|
73
|
+
Properties: { InstanceType: "t3.micro", SubnetId: "subnet-123" },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
78
|
+
// SubnetRequired satisfied; SpotExcludesPlacement not triggered (no InstanceMarketOptions)
|
|
79
|
+
expect(diags).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("if_then: no diagnostic when condition does not match", () => {
|
|
83
|
+
const ctx = makeCtx({
|
|
84
|
+
Resources: {
|
|
85
|
+
MyInstance: {
|
|
86
|
+
Type: "AWS::EC2::Instance",
|
|
87
|
+
Properties: { ImageId: "ami-123" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
19
92
|
expect(diags).toHaveLength(0);
|
|
20
93
|
});
|
|
21
94
|
|
|
22
|
-
|
|
95
|
+
// --- dependent_excluded ---
|
|
96
|
+
|
|
97
|
+
test("dependent_excluded: flags when both present", () => {
|
|
98
|
+
const ctx = makeCtx({
|
|
99
|
+
Resources: {
|
|
100
|
+
MyInstance: {
|
|
101
|
+
Type: "AWS::EC2::Instance",
|
|
102
|
+
Properties: {
|
|
103
|
+
InstanceMarketOptions: { MarketType: "spot" },
|
|
104
|
+
PlacementGroup: "my-group",
|
|
105
|
+
SubnetId: "subnet-123",
|
|
106
|
+
InstanceType: "t3.micro",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
112
|
+
const excluded = diags.filter((d) => d.message.includes("excludes"));
|
|
113
|
+
expect(excluded).toHaveLength(1);
|
|
114
|
+
expect(excluded[0].message).toContain("InstanceMarketOptions");
|
|
115
|
+
expect(excluded[0].message).toContain("PlacementGroup");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("dependent_excluded: no diagnostic when excluded prop absent", () => {
|
|
119
|
+
const ctx = makeCtx({
|
|
120
|
+
Resources: {
|
|
121
|
+
MyInstance: {
|
|
122
|
+
Type: "AWS::EC2::Instance",
|
|
123
|
+
Properties: {
|
|
124
|
+
InstanceMarketOptions: { MarketType: "spot" },
|
|
125
|
+
SubnetId: "subnet-123",
|
|
126
|
+
InstanceType: "t3.micro",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
132
|
+
const excluded = diags.filter((d) => d.message.includes("excludes"));
|
|
133
|
+
expect(excluded).toHaveLength(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// --- required_xor ---
|
|
137
|
+
|
|
138
|
+
test("required_xor: flags when neither present", () => {
|
|
139
|
+
const ctx = makeCtx({
|
|
140
|
+
Resources: {
|
|
141
|
+
MySvc: {
|
|
142
|
+
Type: "AWS::ECS::Service",
|
|
143
|
+
Properties: { ServiceName: "my-svc" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
148
|
+
expect(diags).toHaveLength(1);
|
|
149
|
+
expect(diags[0].message).toContain("exactly one of");
|
|
150
|
+
expect(diags[0].message).toContain("found 0");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("required_xor: flags when both present", () => {
|
|
23
154
|
const ctx = makeCtx({
|
|
24
155
|
Resources: {
|
|
25
|
-
|
|
26
|
-
Type: "
|
|
27
|
-
Properties: {
|
|
156
|
+
MySvc: {
|
|
157
|
+
Type: "AWS::ECS::Service",
|
|
158
|
+
Properties: {
|
|
159
|
+
LaunchType: "FARGATE",
|
|
160
|
+
CapacityProviderStrategy: [{}],
|
|
161
|
+
},
|
|
28
162
|
},
|
|
29
163
|
},
|
|
30
164
|
});
|
|
31
|
-
const diags =
|
|
165
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
166
|
+
expect(diags).toHaveLength(1);
|
|
167
|
+
expect(diags[0].message).toContain("found 2");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("required_xor: no diagnostic when exactly one present", () => {
|
|
171
|
+
const ctx = makeCtx({
|
|
172
|
+
Resources: {
|
|
173
|
+
MySvc: {
|
|
174
|
+
Type: "AWS::ECS::Service",
|
|
175
|
+
Properties: { LaunchType: "FARGATE" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
32
180
|
expect(diags).toHaveLength(0);
|
|
33
181
|
});
|
|
34
182
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
183
|
+
// --- required_or ---
|
|
184
|
+
|
|
185
|
+
test("required_or: flags when none present", () => {
|
|
186
|
+
const ctx = makeCtx({
|
|
187
|
+
Resources: {
|
|
188
|
+
MyBucket: {
|
|
189
|
+
Type: "AWS::S3::Bucket",
|
|
190
|
+
Properties: { BucketName: "test" },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
195
|
+
expect(diags).toHaveLength(1);
|
|
196
|
+
expect(diags[0].message).toContain("at least one of");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("required_or: no diagnostic when one present", () => {
|
|
41
200
|
const ctx = makeCtx({
|
|
42
201
|
Resources: {
|
|
43
202
|
MyBucket: {
|
|
44
203
|
Type: "AWS::S3::Bucket",
|
|
204
|
+
Properties: { BucketEncryption: {} },
|
|
45
205
|
},
|
|
46
206
|
},
|
|
47
207
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
208
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
209
|
+
expect(diags).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// --- Edge cases ---
|
|
213
|
+
|
|
214
|
+
test("no diagnostics for resource type not in constraints", () => {
|
|
215
|
+
const ctx = makeCtx({
|
|
216
|
+
Resources: {
|
|
217
|
+
MyRole: {
|
|
218
|
+
Type: "AWS::IAM::Role",
|
|
219
|
+
Properties: { RoleName: "test" },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
224
|
+
expect(diags).toHaveLength(0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("no diagnostics on empty template", () => {
|
|
228
|
+
const ctx = makeCtx({ Resources: {} });
|
|
229
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
230
|
+
expect(diags).toHaveLength(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("returns empty when constraint map is empty", () => {
|
|
234
|
+
const ctx = makeCtx({
|
|
235
|
+
Resources: {
|
|
236
|
+
MyInstance: {
|
|
237
|
+
Type: "AWS::EC2::Instance",
|
|
238
|
+
Properties: { InstanceType: "t3.micro" },
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
const diags = checkExtensionConstraints(ctx, new Map());
|
|
243
|
+
expect(diags).toHaveLength(0);
|
|
51
244
|
});
|
|
52
245
|
|
|
53
246
|
test("handles invalid JSON output gracefully", () => {
|
|
54
|
-
const ctx:
|
|
55
|
-
|
|
56
|
-
entities: new Map(),
|
|
57
|
-
buildResult: {
|
|
58
|
-
outputs: new Map([["aws", "not json"]]),
|
|
59
|
-
entities: new Map(),
|
|
60
|
-
warnings: [],
|
|
61
|
-
errors: [],
|
|
62
|
-
sourceFileCount: 0,
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
const diags = ext001.check(ctx);
|
|
247
|
+
const ctx = createPostSynthContext({ aws: "not json" as unknown as object });
|
|
248
|
+
const diags = checkExtensionConstraints(ctx, fakeConstraints());
|
|
66
249
|
expect(diags).toHaveLength(0);
|
|
67
250
|
});
|
|
68
251
|
});
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* - required_xor: exactly one of the listed properties must exist
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { readFileSync } from "fs";
|
|
15
|
+
import { join } from "path";
|
|
14
16
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
15
17
|
import { parseCFTemplate, type CFResource } from "./cf-refs";
|
|
16
18
|
|
|
17
|
-
interface ExtensionConstraint {
|
|
19
|
+
export interface ExtensionConstraint {
|
|
18
20
|
name: string;
|
|
19
21
|
type: "if_then" | "dependent_excluded" | "required_or" | "required_xor";
|
|
20
22
|
condition?: unknown;
|
|
@@ -23,7 +25,7 @@ interface ExtensionConstraint {
|
|
|
23
25
|
|
|
24
26
|
interface LexiconEntry {
|
|
25
27
|
kind: string;
|
|
26
|
-
|
|
28
|
+
resourceType: string;
|
|
27
29
|
constraints?: ExtensionConstraint[];
|
|
28
30
|
[key: string]: unknown;
|
|
29
31
|
}
|
|
@@ -34,10 +36,6 @@ interface LexiconEntry {
|
|
|
34
36
|
function loadLexiconConstraints(): Map<string, ExtensionConstraint[]> {
|
|
35
37
|
const map = new Map<string, ExtensionConstraint[]>();
|
|
36
38
|
try {
|
|
37
|
-
const { readFileSync } = require("fs");
|
|
38
|
-
const { join, dirname } = require("path");
|
|
39
|
-
const { fileURLToPath } = require("url");
|
|
40
|
-
|
|
41
39
|
// Navigate from src/lint/post-synth/ up to the package root
|
|
42
40
|
const pkgDir = join(__dirname, "..", "..", "..");
|
|
43
41
|
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
@@ -45,8 +43,8 @@ function loadLexiconConstraints(): Map<string, ExtensionConstraint[]> {
|
|
|
45
43
|
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
46
44
|
|
|
47
45
|
for (const [_name, entry] of Object.entries(data)) {
|
|
48
|
-
if (entry.kind === "resource" && entry.
|
|
49
|
-
map.set(entry.
|
|
46
|
+
if (entry.kind === "resource" && entry.resourceType && entry.constraints && entry.constraints.length > 0) {
|
|
47
|
+
map.set(entry.resourceType, entry.constraints);
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
50
|
} catch {
|
|
@@ -195,28 +193,37 @@ function validateResource(
|
|
|
195
193
|
return diagnostics;
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
198
|
+
*/
|
|
199
|
+
export function checkExtensionConstraints(
|
|
200
|
+
ctx: PostSynthContext,
|
|
201
|
+
constraintMap: Map<string, ExtensionConstraint[]>,
|
|
202
|
+
): PostSynthDiagnostic[] {
|
|
203
|
+
if (constraintMap.size === 0) return [];
|
|
205
204
|
|
|
206
|
-
|
|
205
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
208
|
+
const template = parseCFTemplate(output);
|
|
209
|
+
if (!template?.Resources) continue;
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
212
|
+
const constraints = constraintMap.get(resource.Type);
|
|
213
|
+
if (!constraints) continue;
|
|
215
214
|
|
|
216
|
-
|
|
217
|
-
}
|
|
215
|
+
diagnostics.push(...validateResource(logicalId, resource, constraints));
|
|
218
216
|
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return diagnostics;
|
|
220
|
+
}
|
|
219
221
|
|
|
220
|
-
|
|
222
|
+
export const ext001: PostSynthCheck = {
|
|
223
|
+
id: "EXT001",
|
|
224
|
+
description: "Extension constraint violation — cross-property validation from cfn-lint extension schemas",
|
|
225
|
+
|
|
226
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
227
|
+
return checkExtensionConstraints(ctx, loadLexiconConstraints());
|
|
221
228
|
},
|
|
222
229
|
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { CHILD_PROJECT_MARKER, type ChildProjectInstance } from "@intentius/chant/child-project";
|
|
5
|
+
import { STACK_OUTPUT_MARKER, type StackOutput } from "@intentius/chant/stack-output";
|
|
6
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
7
|
+
import { waw013 } from "./waw013";
|
|
8
|
+
|
|
9
|
+
function makeStackOutput(): StackOutput {
|
|
10
|
+
return {
|
|
11
|
+
[STACK_OUTPUT_MARKER]: true,
|
|
12
|
+
[DECLARABLE_MARKER]: true,
|
|
13
|
+
lexicon: "aws",
|
|
14
|
+
entityType: "chant:output",
|
|
15
|
+
kind: "output",
|
|
16
|
+
sourceRef: {} as any,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeChildProject(opts: {
|
|
21
|
+
projectPath?: string;
|
|
22
|
+
childEntities?: Map<string, Declarable>;
|
|
23
|
+
}): ChildProjectInstance {
|
|
24
|
+
return {
|
|
25
|
+
[CHILD_PROJECT_MARKER]: true,
|
|
26
|
+
[DECLARABLE_MARKER]: true,
|
|
27
|
+
lexicon: "aws",
|
|
28
|
+
entityType: "AWS::CloudFormation::Stack",
|
|
29
|
+
kind: "resource",
|
|
30
|
+
projectPath: opts.projectPath ?? "/tmp/child",
|
|
31
|
+
logicalName: "Child",
|
|
32
|
+
outputs: {},
|
|
33
|
+
options: {},
|
|
34
|
+
buildResult: {
|
|
35
|
+
outputs: new Map(),
|
|
36
|
+
entities: opts.childEntities ?? new Map(),
|
|
37
|
+
warnings: [],
|
|
38
|
+
errors: [],
|
|
39
|
+
sourceFileCount: 1,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeCtx(entities: Map<string, Declarable>): PostSynthContext {
|
|
45
|
+
return {
|
|
46
|
+
outputs: new Map(),
|
|
47
|
+
entities,
|
|
48
|
+
buildResult: {
|
|
49
|
+
outputs: new Map(),
|
|
50
|
+
entities,
|
|
51
|
+
warnings: [],
|
|
52
|
+
errors: [],
|
|
53
|
+
sourceFileCount: 1,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("WAW013: Child project has no stackOutput() exports", () => {
|
|
59
|
+
test("check metadata", () => {
|
|
60
|
+
expect(waw013.id).toBe("WAW013");
|
|
61
|
+
expect(waw013.description).toContain("stackOutput");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("flags child project with no outputs", () => {
|
|
65
|
+
const child = makeChildProject({ childEntities: new Map() });
|
|
66
|
+
const ctx = makeCtx(new Map([["Network", child]]));
|
|
67
|
+
const diags = waw013.check(ctx);
|
|
68
|
+
expect(diags).toHaveLength(1);
|
|
69
|
+
expect(diags[0].checkId).toBe("WAW013");
|
|
70
|
+
expect(diags[0].severity).toBe("error");
|
|
71
|
+
expect(diags[0].message).toContain("Network");
|
|
72
|
+
expect(diags[0].message).toContain("no stackOutput()");
|
|
73
|
+
expect(diags[0].entity).toBe("Network");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("no diagnostic when child has stackOutput", () => {
|
|
77
|
+
const childEntities = new Map<string, Declarable>([
|
|
78
|
+
["subnetId", makeStackOutput()],
|
|
79
|
+
]);
|
|
80
|
+
const child = makeChildProject({ childEntities });
|
|
81
|
+
const ctx = makeCtx(new Map([["Network", child]]));
|
|
82
|
+
const diags = waw013.check(ctx);
|
|
83
|
+
expect(diags).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("no diagnostic when no child projects", () => {
|
|
87
|
+
const ctx = makeCtx(new Map());
|
|
88
|
+
const diags = waw013.check(ctx);
|
|
89
|
+
expect(diags).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("only flags children without outputs, not those with", () => {
|
|
93
|
+
const goodChild = makeChildProject({
|
|
94
|
+
projectPath: "/tmp/good",
|
|
95
|
+
childEntities: new Map([["out", makeStackOutput()]]),
|
|
96
|
+
});
|
|
97
|
+
const badChild = makeChildProject({
|
|
98
|
+
projectPath: "/tmp/bad",
|
|
99
|
+
childEntities: new Map(),
|
|
100
|
+
});
|
|
101
|
+
const ctx = makeCtx(new Map<string, Declarable>([
|
|
102
|
+
["Good", goodChild],
|
|
103
|
+
["Bad", badChild],
|
|
104
|
+
]));
|
|
105
|
+
const diags = waw013.check(ctx);
|
|
106
|
+
expect(diags).toHaveLength(1);
|
|
107
|
+
expect(diags[0].entity).toBe("Bad");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("skips non-child-project entities", () => {
|
|
111
|
+
const plainEntity: Declarable = {
|
|
112
|
+
[DECLARABLE_MARKER]: true,
|
|
113
|
+
lexicon: "aws",
|
|
114
|
+
entityType: "AWS::S3::Bucket",
|
|
115
|
+
};
|
|
116
|
+
const ctx = makeCtx(new Map([["MyBucket", plainEntity]]));
|
|
117
|
+
const diags = waw013.check(ctx);
|
|
118
|
+
expect(diags).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { CHILD_PROJECT_MARKER, type ChildProjectInstance } from "@intentius/chant/child-project";
|
|
5
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
6
|
+
import { waw014 } from "./waw014";
|
|
7
|
+
|
|
8
|
+
function makeChildProject(projectPath = "/tmp/child"): ChildProjectInstance {
|
|
9
|
+
return {
|
|
10
|
+
[CHILD_PROJECT_MARKER]: true,
|
|
11
|
+
[DECLARABLE_MARKER]: true,
|
|
12
|
+
lexicon: "aws",
|
|
13
|
+
entityType: "AWS::CloudFormation::Stack",
|
|
14
|
+
kind: "resource",
|
|
15
|
+
projectPath,
|
|
16
|
+
logicalName: "Child",
|
|
17
|
+
outputs: {},
|
|
18
|
+
options: {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeCtx(
|
|
23
|
+
entities: Map<string, Declarable>,
|
|
24
|
+
templateJson: string,
|
|
25
|
+
): PostSynthContext {
|
|
26
|
+
const outputs = new Map<string, string>([["aws", templateJson]]);
|
|
27
|
+
return {
|
|
28
|
+
outputs,
|
|
29
|
+
entities,
|
|
30
|
+
buildResult: {
|
|
31
|
+
outputs,
|
|
32
|
+
entities,
|
|
33
|
+
warnings: [],
|
|
34
|
+
errors: [],
|
|
35
|
+
sourceFileCount: 1,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("WAW014: Unreferenced Nested Stack Outputs", () => {
|
|
41
|
+
test("check metadata", () => {
|
|
42
|
+
expect(waw014.id).toBe("WAW014");
|
|
43
|
+
expect(waw014.description).toContain("outputs");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("flags child whose outputs are never referenced in parent template", () => {
|
|
47
|
+
const entities = new Map<string, Declarable>([
|
|
48
|
+
["Network", makeChildProject()],
|
|
49
|
+
]);
|
|
50
|
+
// Parent template doesn't reference Network via Fn::GetAtt
|
|
51
|
+
const template = JSON.stringify({
|
|
52
|
+
Resources: {
|
|
53
|
+
Network: {
|
|
54
|
+
Type: "AWS::CloudFormation::Stack",
|
|
55
|
+
Properties: { TemplateURL: "network.json" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const ctx = makeCtx(entities, template);
|
|
60
|
+
const diags = waw014.check(ctx);
|
|
61
|
+
expect(diags).toHaveLength(1);
|
|
62
|
+
expect(diags[0].checkId).toBe("WAW014");
|
|
63
|
+
expect(diags[0].severity).toBe("warning");
|
|
64
|
+
expect(diags[0].message).toContain("Network");
|
|
65
|
+
expect(diags[0].message).toContain("never referenced");
|
|
66
|
+
expect(diags[0].entity).toBe("Network");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("no diagnostic when child outputs are referenced via Fn::GetAtt", () => {
|
|
70
|
+
const entities = new Map<string, Declarable>([
|
|
71
|
+
["Network", makeChildProject()],
|
|
72
|
+
]);
|
|
73
|
+
const template = JSON.stringify({
|
|
74
|
+
Resources: {
|
|
75
|
+
Network: {
|
|
76
|
+
Type: "AWS::CloudFormation::Stack",
|
|
77
|
+
Properties: { TemplateURL: "network.json" },
|
|
78
|
+
},
|
|
79
|
+
MyFunc: {
|
|
80
|
+
Type: "AWS::Lambda::Function",
|
|
81
|
+
Properties: {
|
|
82
|
+
VpcConfig: {
|
|
83
|
+
SubnetIds: [{ "Fn::GetAtt": ["Network", "Outputs.SubnetId"] }],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const ctx = makeCtx(entities, template);
|
|
90
|
+
const diags = waw014.check(ctx);
|
|
91
|
+
expect(diags).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("no diagnostic when no child projects", () => {
|
|
95
|
+
const ctx = makeCtx(new Map(), JSON.stringify({ Resources: {} }));
|
|
96
|
+
const diags = waw014.check(ctx);
|
|
97
|
+
expect(diags).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("flags only unreferenced children, not referenced ones", () => {
|
|
101
|
+
const entities = new Map<string, Declarable>([
|
|
102
|
+
["Network", makeChildProject("/tmp/net")],
|
|
103
|
+
["Database", makeChildProject("/tmp/db")],
|
|
104
|
+
]);
|
|
105
|
+
// Only Network is referenced
|
|
106
|
+
const template = JSON.stringify({
|
|
107
|
+
Resources: {
|
|
108
|
+
MyFunc: {
|
|
109
|
+
Type: "AWS::Lambda::Function",
|
|
110
|
+
Properties: {
|
|
111
|
+
SubnetId: { "Fn::GetAtt": ["Network", "Outputs.SubnetId"] },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const ctx = makeCtx(entities, template);
|
|
117
|
+
const diags = waw014.check(ctx);
|
|
118
|
+
expect(diags).toHaveLength(1);
|
|
119
|
+
expect(diags[0].entity).toBe("Database");
|
|
120
|
+
});
|
|
121
|
+
});
|