@intentius/chant-lexicon-aws 0.0.2

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.
Files changed (94) hide show
  1. package/README.md +438 -0
  2. package/package.json +30 -0
  3. package/src/codegen/__snapshots__/snapshot.test.ts.snap +197 -0
  4. package/src/codegen/docs-cli.ts +3 -0
  5. package/src/codegen/docs.ts +1206 -0
  6. package/src/codegen/extensions.ts +171 -0
  7. package/src/codegen/fallback.ts +33 -0
  8. package/src/codegen/generate-cli.ts +17 -0
  9. package/src/codegen/generate-lexicon.ts +98 -0
  10. package/src/codegen/generate-typescript.ts +257 -0
  11. package/src/codegen/generate.test.ts +125 -0
  12. package/src/codegen/generate.ts +226 -0
  13. package/src/codegen/idempotency.test.ts +28 -0
  14. package/src/codegen/naming.ts +120 -0
  15. package/src/codegen/package.test.ts +60 -0
  16. package/src/codegen/package.ts +84 -0
  17. package/src/codegen/patches.ts +98 -0
  18. package/src/codegen/rollback.test.ts +80 -0
  19. package/src/codegen/rollback.ts +20 -0
  20. package/src/codegen/sam.ts +387 -0
  21. package/src/codegen/snapshot.test.ts +84 -0
  22. package/src/codegen/typecheck.test.ts +50 -0
  23. package/src/codegen/typecheck.ts +4 -0
  24. package/src/codegen/versions.ts +37 -0
  25. package/src/coverage.ts +14 -0
  26. package/src/generated/index.d.ts +160753 -0
  27. package/src/generated/index.ts +14396 -0
  28. package/src/generated/lexicon-aws.json +114563 -0
  29. package/src/generated/runtime.ts +4 -0
  30. package/src/import/generator.test.ts +181 -0
  31. package/src/import/generator.ts +349 -0
  32. package/src/import/parser.test.ts +200 -0
  33. package/src/import/parser.ts +350 -0
  34. package/src/import/roundtrip-fixtures.test.ts +78 -0
  35. package/src/import/roundtrip.test.ts +195 -0
  36. package/src/index.ts +63 -0
  37. package/src/integration.test.ts +129 -0
  38. package/src/intrinsics.test.ts +167 -0
  39. package/src/intrinsics.ts +223 -0
  40. package/src/lint/post-synth/cf-refs.ts +91 -0
  41. package/src/lint/post-synth/cor020.ts +72 -0
  42. package/src/lint/post-synth/ext001.test.ts +68 -0
  43. package/src/lint/post-synth/ext001.ts +222 -0
  44. package/src/lint/post-synth/post-synth.test.ts +280 -0
  45. package/src/lint/post-synth/waw010.ts +49 -0
  46. package/src/lint/post-synth/waw011.ts +49 -0
  47. package/src/lint/post-synth/waw013.ts +45 -0
  48. package/src/lint/post-synth/waw014.ts +50 -0
  49. package/src/lint/post-synth/waw015.ts +100 -0
  50. package/src/lint/rules/hardcoded-region.ts +43 -0
  51. package/src/lint/rules/iam-wildcard.ts +66 -0
  52. package/src/lint/rules/index.ts +7 -0
  53. package/src/lint/rules/rules.test.ts +175 -0
  54. package/src/lint/rules/s3-encryption.ts +69 -0
  55. package/src/lsp/completions.test.ts +72 -0
  56. package/src/lsp/completions.ts +18 -0
  57. package/src/lsp/hover.test.ts +53 -0
  58. package/src/lsp/hover.ts +53 -0
  59. package/src/nested-stack.test.ts +83 -0
  60. package/src/nested-stack.ts +125 -0
  61. package/src/plugin.test.ts +316 -0
  62. package/src/plugin.ts +514 -0
  63. package/src/pseudo.test.ts +55 -0
  64. package/src/pseudo.ts +29 -0
  65. package/src/serializer.test.ts +507 -0
  66. package/src/serializer.ts +333 -0
  67. package/src/spec/fetch.test.ts +27 -0
  68. package/src/spec/fetch.ts +107 -0
  69. package/src/spec/parse.test.ts +153 -0
  70. package/src/spec/parse.ts +202 -0
  71. package/src/testdata/load-fixtures.ts +17 -0
  72. package/src/testdata/roundtrip/conditions.json +21 -0
  73. package/src/testdata/roundtrip/intrinsic-calls.json +31 -0
  74. package/src/testdata/roundtrip/intrinsics.json +18 -0
  75. package/src/testdata/roundtrip/multi-resource.json +37 -0
  76. package/src/testdata/roundtrip/parameters.json +23 -0
  77. package/src/testdata/roundtrip/simple.json +12 -0
  78. package/src/testdata/sam-fixtures/api.yaml +14 -0
  79. package/src/testdata/sam-fixtures/application.yaml +13 -0
  80. package/src/testdata/sam-fixtures/function.yaml +22 -0
  81. package/src/testdata/sam-fixtures/graphql-api.yaml +13 -0
  82. package/src/testdata/sam-fixtures/http-api.yaml +15 -0
  83. package/src/testdata/sam-fixtures/layer-version.yaml +15 -0
  84. package/src/testdata/sam-fixtures/multi-type-a.yaml +23 -0
  85. package/src/testdata/sam-fixtures/multi-type-b.yaml +29 -0
  86. package/src/testdata/sam-fixtures/simple-table.yaml +12 -0
  87. package/src/testdata/sam-fixtures/state-machine.yaml +14 -0
  88. package/src/testdata/schemas/aws-dynamodb-table.json +126 -0
  89. package/src/testdata/schemas/aws-iam-role.json +85 -0
  90. package/src/testdata/schemas/aws-lambda-function.json +90 -0
  91. package/src/testdata/schemas/aws-s3-bucket.json +83 -0
  92. package/src/testdata/schemas/aws-sns-topic.json +71 -0
  93. package/src/validate-cli.ts +19 -0
  94. package/src/validate.ts +34 -0
@@ -0,0 +1,280 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw010 } from "./waw010";
4
+ import { waw011 } from "./waw011";
5
+ import { cor020 } from "./cor020";
6
+ import { findResourceRefs, parseCFTemplate } from "./cf-refs";
7
+
8
+ function makeCtx(template: object) {
9
+ return createPostSynthContext({ aws: template });
10
+ }
11
+
12
+ // --- cf-refs utility tests ---
13
+
14
+ describe("findResourceRefs", () => {
15
+ test("extracts Ref targets", () => {
16
+ const refs = findResourceRefs({ Ref: "MyBucket" });
17
+ expect(refs.has("MyBucket")).toBe(true);
18
+ });
19
+
20
+ test("extracts Fn::GetAtt array form", () => {
21
+ const refs = findResourceRefs({ "Fn::GetAtt": ["MyRole", "Arn"] });
22
+ expect(refs.has("MyRole")).toBe(true);
23
+ });
24
+
25
+ test("extracts Fn::GetAtt dot form", () => {
26
+ const refs = findResourceRefs({ "Fn::GetAtt": "MyRole.Arn" });
27
+ expect(refs.has("MyRole")).toBe(true);
28
+ });
29
+
30
+ test("skips pseudo-parameters", () => {
31
+ const refs = findResourceRefs({ Ref: "AWS::StackName" });
32
+ expect(refs.size).toBe(0);
33
+ });
34
+
35
+ test("recurses into nested structures", () => {
36
+ const refs = findResourceRefs({
37
+ "Fn::Join": ["-", [{ Ref: "A" }, { "Fn::GetAtt": ["B", "Arn"] }]],
38
+ });
39
+ expect(refs.has("A")).toBe(true);
40
+ expect(refs.has("B")).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe("parseCFTemplate", () => {
45
+ test("parses valid JSON", () => {
46
+ const t = parseCFTemplate('{"Resources":{}}');
47
+ expect(t).toBeTruthy();
48
+ expect(t!.Resources).toEqual({});
49
+ });
50
+
51
+ test("returns null for invalid JSON", () => {
52
+ expect(parseCFTemplate("not json")).toBeNull();
53
+ });
54
+ });
55
+
56
+ // --- WAW010: Redundant DependsOn ---
57
+
58
+ describe("WAW010: Redundant DependsOn", () => {
59
+ test("detects redundant DependsOn from Ref", () => {
60
+ const ctx = makeCtx({
61
+ Resources: {
62
+ MyFunction: {
63
+ Type: "AWS::Lambda::Function",
64
+ DependsOn: ["MyRole"],
65
+ Properties: {
66
+ Role: { "Fn::GetAtt": ["MyRole", "Arn"] },
67
+ },
68
+ },
69
+ MyRole: {
70
+ Type: "AWS::IAM::Role",
71
+ Properties: {},
72
+ },
73
+ },
74
+ });
75
+
76
+ const diags = waw010.check(ctx);
77
+ expect(diags).toHaveLength(1);
78
+ expect(diags[0].checkId).toBe("WAW010");
79
+ expect(diags[0].severity).toBe("warning");
80
+ expect(diags[0].message).toContain("redundant DependsOn");
81
+ expect(diags[0].message).toContain("MyRole");
82
+ });
83
+
84
+ test("no diagnostic when DependsOn is not redundant", () => {
85
+ const ctx = makeCtx({
86
+ Resources: {
87
+ MyFunction: {
88
+ Type: "AWS::Lambda::Function",
89
+ DependsOn: ["MyTable"],
90
+ Properties: {
91
+ Role: { "Fn::GetAtt": ["MyRole", "Arn"] },
92
+ },
93
+ },
94
+ MyRole: { Type: "AWS::IAM::Role", Properties: {} },
95
+ MyTable: { Type: "AWS::DynamoDB::Table", Properties: {} },
96
+ },
97
+ });
98
+
99
+ const diags = waw010.check(ctx);
100
+ expect(diags).toHaveLength(0);
101
+ });
102
+
103
+ test("handles string DependsOn (not array)", () => {
104
+ const ctx = makeCtx({
105
+ Resources: {
106
+ A: {
107
+ Type: "AWS::Lambda::Function",
108
+ DependsOn: "B",
109
+ Properties: { X: { Ref: "B" } },
110
+ },
111
+ B: { Type: "AWS::S3::Bucket", Properties: {} },
112
+ },
113
+ });
114
+
115
+ const diags = waw010.check(ctx);
116
+ expect(diags).toHaveLength(1);
117
+ });
118
+ });
119
+
120
+ // --- WAW011: Deprecated Lambda Runtime ---
121
+
122
+ describe("WAW011: Deprecated Lambda Runtime", () => {
123
+ test("emits error for deprecated runtime", () => {
124
+ const ctx = makeCtx({
125
+ Resources: {
126
+ MyFunc: {
127
+ Type: "AWS::Lambda::Function",
128
+ Properties: { Runtime: "nodejs14.x" },
129
+ },
130
+ },
131
+ });
132
+
133
+ const diags = waw011.check(ctx);
134
+ expect(diags).toHaveLength(1);
135
+ expect(diags[0].checkId).toBe("WAW011");
136
+ expect(diags[0].severity).toBe("error");
137
+ expect(diags[0].message).toContain("deprecated");
138
+ expect(diags[0].message).toContain("nodejs14.x");
139
+ });
140
+
141
+ test("emits warning for approaching-EOL runtime", () => {
142
+ const ctx = makeCtx({
143
+ Resources: {
144
+ MyFunc: {
145
+ Type: "AWS::Lambda::Function",
146
+ Properties: { Runtime: "nodejs18.x" },
147
+ },
148
+ },
149
+ });
150
+
151
+ const diags = waw011.check(ctx);
152
+ expect(diags).toHaveLength(1);
153
+ expect(diags[0].severity).toBe("warning");
154
+ expect(diags[0].message).toContain("approaching end-of-life");
155
+ });
156
+
157
+ test("no diagnostic for current runtime", () => {
158
+ const ctx = makeCtx({
159
+ Resources: {
160
+ MyFunc: {
161
+ Type: "AWS::Lambda::Function",
162
+ Properties: { Runtime: "nodejs22.x" },
163
+ },
164
+ },
165
+ });
166
+
167
+ const diags = waw011.check(ctx);
168
+ expect(diags).toHaveLength(0);
169
+ });
170
+
171
+ test("skips non-Lambda resources", () => {
172
+ const ctx = makeCtx({
173
+ Resources: {
174
+ MyBucket: {
175
+ Type: "AWS::S3::Bucket",
176
+ Properties: { Runtime: "nodejs14.x" },
177
+ },
178
+ },
179
+ });
180
+
181
+ const diags = waw011.check(ctx);
182
+ expect(diags).toHaveLength(0);
183
+ });
184
+ });
185
+
186
+ // --- COR020: Circular Resource Dependencies ---
187
+
188
+ describe("COR020: Circular Resource Dependencies", () => {
189
+ test("detects simple two-node cycle", () => {
190
+ const ctx = makeCtx({
191
+ Resources: {
192
+ A: {
193
+ Type: "AWS::Lambda::Function",
194
+ Properties: { X: { Ref: "B" } },
195
+ },
196
+ B: {
197
+ Type: "AWS::IAM::Role",
198
+ Properties: { Y: { Ref: "A" } },
199
+ },
200
+ },
201
+ });
202
+
203
+ const diags = cor020.check(ctx);
204
+ expect(diags).toHaveLength(1);
205
+ expect(diags[0].checkId).toBe("COR020");
206
+ expect(diags[0].severity).toBe("error");
207
+ expect(diags[0].message).toContain("Circular resource dependency");
208
+ // Should contain both nodes in the cycle
209
+ expect(diags[0].message).toContain("A");
210
+ expect(diags[0].message).toContain("B");
211
+ });
212
+
213
+ test("detects three-node cycle", () => {
214
+ const ctx = makeCtx({
215
+ Resources: {
216
+ A: {
217
+ Type: "AWS::Lambda::Function",
218
+ Properties: { X: { Ref: "B" } },
219
+ },
220
+ B: {
221
+ Type: "AWS::IAM::Role",
222
+ Properties: { Y: { Ref: "C" } },
223
+ },
224
+ C: {
225
+ Type: "AWS::S3::Bucket",
226
+ DependsOn: ["A"],
227
+ Properties: {},
228
+ },
229
+ },
230
+ });
231
+
232
+ const diags = cor020.check(ctx);
233
+ expect(diags).toHaveLength(1);
234
+ expect(diags[0].message).toContain("A");
235
+ expect(diags[0].message).toContain("B");
236
+ expect(diags[0].message).toContain("C");
237
+ });
238
+
239
+ test("no diagnostic for acyclic graph", () => {
240
+ const ctx = makeCtx({
241
+ Resources: {
242
+ A: {
243
+ Type: "AWS::Lambda::Function",
244
+ Properties: { X: { Ref: "B" } },
245
+ },
246
+ B: {
247
+ Type: "AWS::IAM::Role",
248
+ Properties: { Y: { Ref: "C" } },
249
+ },
250
+ C: {
251
+ Type: "AWS::S3::Bucket",
252
+ Properties: {},
253
+ },
254
+ },
255
+ });
256
+
257
+ const diags = cor020.check(ctx);
258
+ expect(diags).toHaveLength(0);
259
+ });
260
+
261
+ test("handles DependsOn-based cycles", () => {
262
+ const ctx = makeCtx({
263
+ Resources: {
264
+ A: {
265
+ Type: "AWS::Lambda::Function",
266
+ DependsOn: ["B"],
267
+ Properties: {},
268
+ },
269
+ B: {
270
+ Type: "AWS::IAM::Role",
271
+ DependsOn: ["A"],
272
+ Properties: {},
273
+ },
274
+ },
275
+ });
276
+
277
+ const diags = cor020.check(ctx);
278
+ expect(diags).toHaveLength(1);
279
+ });
280
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WAW010: Redundant DependsOn
3
+ *
4
+ * Detects DependsOn entries that are already implied by Ref or Fn::GetAtt
5
+ * references in the resource's Properties. CloudFormation automatically
6
+ * creates dependencies for these references, making explicit DependsOn redundant.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate, findResourceRefs } from "./cf-refs";
11
+
12
+ export const waw010: PostSynthCheck = {
13
+ id: "WAW010",
14
+ description: "Redundant DependsOn — target is already referenced via Ref or Fn::GetAtt in Properties",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [_lexicon, output] of ctx.outputs) {
20
+ const template = parseCFTemplate(output);
21
+ if (!template?.Resources) continue;
22
+
23
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
24
+ if (!resource.DependsOn) continue;
25
+
26
+ const dependsOn = Array.isArray(resource.DependsOn)
27
+ ? resource.DependsOn
28
+ : [resource.DependsOn];
29
+
30
+ // Find all refs in Properties
31
+ const propertyRefs = findResourceRefs(resource.Properties);
32
+
33
+ for (const target of dependsOn) {
34
+ if (propertyRefs.has(target)) {
35
+ diagnostics.push({
36
+ checkId: "WAW010",
37
+ severity: "warning",
38
+ message: `Resource "${logicalId}" has redundant DependsOn "${target}" — already referenced in Properties`,
39
+ entity: logicalId,
40
+ lexicon: "aws",
41
+ });
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WAW011: Deprecated Lambda Runtime
3
+ *
4
+ * Checks Lambda function resources for deprecated or approaching-EOL runtimes.
5
+ * Emits error for "deprecated" runtimes and warning for "approaching_eol".
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import type { Severity } from "@intentius/chant/lint/rule";
10
+ import { parseCFTemplate } from "./cf-refs";
11
+ import { lambdaRuntimeDeprecations } from "../../codegen/generate-lexicon";
12
+
13
+ export const waw011: PostSynthCheck = {
14
+ id: "WAW011",
15
+ description: "Deprecated Lambda runtime — flags deprecated or approaching-EOL Lambda runtimes",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+ const deprecations = lambdaRuntimeDeprecations();
20
+
21
+ for (const [_lexicon, output] of ctx.outputs) {
22
+ const template = parseCFTemplate(output);
23
+ if (!template?.Resources) continue;
24
+
25
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
26
+ if (resource.Type !== "AWS::Lambda::Function") continue;
27
+
28
+ const runtime = resource.Properties?.Runtime;
29
+ if (typeof runtime !== "string") continue;
30
+
31
+ const status = deprecations[runtime];
32
+ if (!status) continue;
33
+
34
+ const severity: Severity = status === "deprecated" ? "error" : "warning";
35
+ const label = status === "deprecated" ? "deprecated" : "approaching end-of-life";
36
+
37
+ diagnostics.push({
38
+ checkId: "WAW011",
39
+ severity,
40
+ message: `Lambda "${logicalId}" uses ${label} runtime "${runtime}"`,
41
+ entity: logicalId,
42
+ lexicon: "aws",
43
+ });
44
+ }
45
+ }
46
+
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * WAW013: Child project has no stackOutput() exports
3
+ *
4
+ * When a parent uses nestedStack() to reference a child project, the child
5
+ * must declare at least one stackOutput() — otherwise the parent can't
6
+ * reference any of its values.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { isChildProject } from "@intentius/chant/child-project";
11
+ import { isStackOutput } from "@intentius/chant/stack-output";
12
+
13
+ export const waw013: PostSynthCheck = {
14
+ id: "WAW013",
15
+ description: "Child project has no stackOutput() exports — parent can't reference anything",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [name, entity] of ctx.entities) {
21
+ if (isChildProject(entity) && entity.buildResult) {
22
+ // Check if the child has any StackOutput entities
23
+ let hasOutputs = false;
24
+ for (const [, childEntity] of entity.buildResult.entities) {
25
+ if (isStackOutput(childEntity)) {
26
+ hasOutputs = true;
27
+ break;
28
+ }
29
+ }
30
+
31
+ if (!hasOutputs) {
32
+ diagnostics.push({
33
+ checkId: "WAW013",
34
+ severity: "error",
35
+ message: `Nested stack "${name}" child project has no stackOutput() exports — parent can't reference any values`,
36
+ entity: name,
37
+ lexicon: "aws",
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return diagnostics;
44
+ },
45
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WAW014: Nested stack outputs never referenced from parent
3
+ *
4
+ * Warns when a nestedStack() is declared but none of its outputs are
5
+ * referenced from the parent template. Could just be a separate build.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
10
+ import { isChildProject } from "@intentius/chant/child-project";
11
+
12
+ export const waw014: PostSynthCheck = {
13
+ id: "WAW014",
14
+ description: "Nested stack outputs never referenced from parent — could just be a separate build",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ // Collect child project names
20
+ const childProjectNames = new Set<string>();
21
+ for (const [name, entity] of ctx.entities) {
22
+ if (isChildProject(entity)) {
23
+ childProjectNames.add(name);
24
+ }
25
+ }
26
+
27
+ if (childProjectNames.size === 0) return diagnostics;
28
+
29
+ // Parse the primary template to check for Fn::GetAtt on nested stacks
30
+ for (const [_lexicon, output] of ctx.outputs) {
31
+ const json = getPrimaryOutput(output);
32
+
33
+ for (const stackName of childProjectNames) {
34
+ // Check if any Fn::GetAtt references this stack's outputs
35
+ const hasRef = json.includes(`"Fn::GetAtt"`) && json.includes(`"${stackName}"`);
36
+ if (!hasRef) {
37
+ diagnostics.push({
38
+ checkId: "WAW014",
39
+ severity: "warning",
40
+ message: `Nested stack "${stackName}" outputs are never referenced — consider building it separately`,
41
+ entity: stackName,
42
+ lexicon: "aws",
43
+ });
44
+ }
45
+ }
46
+ }
47
+
48
+ return diagnostics;
49
+ },
50
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * WAW015: Circular project references
3
+ *
4
+ * Detects when child projects form circular references through
5
+ * nestedStack() declarations. A → B → A would cause infinite recursion
6
+ * at build time.
7
+ *
8
+ * Note: The core build pipeline also detects this and emits a BuildError,
9
+ * but this lint check provides a clearer diagnostic message.
10
+ */
11
+
12
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
13
+ import { isChildProject } from "@intentius/chant/child-project";
14
+
15
+ export const waw015: PostSynthCheck = {
16
+ id: "WAW015",
17
+ description: "Circular dependency between nested stacks would cause infinite build recursion",
18
+
19
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+
22
+ // Build a dependency graph: stackName → set of child project paths
23
+ const stacks = new Map<string, string>();
24
+ for (const [name, entity] of ctx.entities) {
25
+ if (isChildProject(entity)) {
26
+ stacks.set(name, entity.projectPath);
27
+ }
28
+ }
29
+
30
+ if (stacks.size < 2) return diagnostics;
31
+
32
+ // Check for cycles by looking at child build results for nested ChildProjectInstances
33
+ const deps = new Map<string, Set<string>>();
34
+ for (const [name] of stacks) {
35
+ deps.set(name, new Set());
36
+ }
37
+
38
+ for (const [name, entity] of ctx.entities) {
39
+ if (isChildProject(entity) && entity.buildResult) {
40
+ for (const [, childEntity] of entity.buildResult.entities) {
41
+ if (isChildProject(childEntity)) {
42
+ // Check if the child references any of our known stacks
43
+ for (const [stackName, stackPath] of stacks) {
44
+ if (childEntity.projectPath === stackPath) {
45
+ deps.get(name)!.add(stackName);
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ // Detect cycles using DFS
54
+ const visited = new Set<string>();
55
+ const inPath = new Set<string>();
56
+
57
+ function dfs(node: string, path: string[]): string[] | null {
58
+ if (inPath.has(node)) {
59
+ const cycleStart = path.indexOf(node);
60
+ return path.slice(cycleStart);
61
+ }
62
+ if (visited.has(node)) return null;
63
+
64
+ visited.add(node);
65
+ inPath.add(node);
66
+ path.push(node);
67
+
68
+ for (const dep of deps.get(node) ?? []) {
69
+ const cycle = dfs(dep, path);
70
+ if (cycle) return cycle;
71
+ }
72
+
73
+ path.pop();
74
+ inPath.delete(node);
75
+ return null;
76
+ }
77
+
78
+ const reportedCycles = new Set<string>();
79
+ for (const [name] of stacks) {
80
+ if (!visited.has(name)) {
81
+ const cycle = dfs(name, []);
82
+ if (cycle) {
83
+ const cycleKey = [...cycle].sort().join(",");
84
+ if (!reportedCycles.has(cycleKey)) {
85
+ reportedCycles.add(cycleKey);
86
+ diagnostics.push({
87
+ checkId: "WAW015",
88
+ severity: "error",
89
+ message: `Circular dependency between nested stacks: ${cycle.join(" → ")} → ${cycle[0]}`,
90
+ entity: cycle[0],
91
+ lexicon: "aws",
92
+ });
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ return diagnostics;
99
+ },
100
+ };
@@ -0,0 +1,43 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * WAW001: Hardcoded AWS Region
6
+ *
7
+ * Detects hardcoded AWS region strings in code.
8
+ * Regions should use AWS.Region pseudo-parameter instead.
9
+ */
10
+ export const hardcodedRegionRule: LintRule = {
11
+ id: "WAW001",
12
+ severity: "warning",
13
+ category: "security",
14
+
15
+ check(context: LintContext): LintDiagnostic[] {
16
+ const { sourceFile } = context;
17
+ const diagnostics: LintDiagnostic[] = [];
18
+
19
+ // Common AWS region patterns
20
+ const regionPattern = /^(us|eu|ap|sa|ca|me|af|cn)-(north|south|east|west|central|northeast|southeast)-\d$/;
21
+
22
+ function visit(node: ts.Node): void {
23
+ if (ts.isStringLiteral(node)) {
24
+ const value = node.text;
25
+ if (regionPattern.test(value)) {
26
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
27
+ diagnostics.push({
28
+ file: sourceFile.fileName,
29
+ line: line + 1,
30
+ column: character + 1,
31
+ ruleId: "WAW001",
32
+ severity: "warning",
33
+ message: `Hardcoded region "${value}" detected. Use AWS.Region pseudo-parameter instead.`,
34
+ });
35
+ }
36
+ }
37
+ ts.forEachChild(node, visit);
38
+ }
39
+
40
+ visit(sourceFile);
41
+ return diagnostics;
42
+ },
43
+ };
@@ -0,0 +1,66 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * WAW009: IAM Wildcard Resource
6
+ *
7
+ * Detects IAM policies with wildcard (*) resources.
8
+ * Policies should specify explicit resources for better security.
9
+ */
10
+ export const iamWildcardRule: LintRule = {
11
+ id: "WAW009",
12
+ severity: "warning",
13
+ category: "security",
14
+
15
+ check(context: LintContext): LintDiagnostic[] {
16
+ const { sourceFile } = context;
17
+ const diagnostics: LintDiagnostic[] = [];
18
+
19
+ function visit(node: ts.Node): void {
20
+ // Look for Resource: "*" in object literals
21
+ if (ts.isPropertyAssignment(node)) {
22
+ const name = node.name;
23
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
24
+ const propName = ts.isIdentifier(name) ? name.text : name.text;
25
+
26
+ // Check for Resource or Resources property
27
+ if (propName.toLowerCase() === "resource" || propName.toLowerCase() === "resources") {
28
+ // Check if value is "*"
29
+ if (ts.isStringLiteral(node.initializer) && node.initializer.text === "*") {
30
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
31
+ diagnostics.push({
32
+ file: sourceFile.fileName,
33
+ line: line + 1,
34
+ column: character + 1,
35
+ ruleId: "WAW009",
36
+ severity: "warning",
37
+ message: "IAM policy uses wildcard (*) resource. Specify explicit resource ARNs for better security.",
38
+ });
39
+ }
40
+ // Check if array contains "*"
41
+ else if (ts.isArrayLiteralExpression(node.initializer)) {
42
+ for (const element of node.initializer.elements) {
43
+ if (ts.isStringLiteral(element) && element.text === "*") {
44
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(element.getStart());
45
+ diagnostics.push({
46
+ file: sourceFile.fileName,
47
+ line: line + 1,
48
+ column: character + 1,
49
+ ruleId: "WAW009",
50
+ severity: "warning",
51
+ message: "IAM policy uses wildcard (*) resource. Specify explicit resource ARNs for better security.",
52
+ });
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ ts.forEachChild(node, visit);
61
+ }
62
+
63
+ visit(sourceFile);
64
+ return diagnostics;
65
+ },
66
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * AWS-specific lint rules for chant projects
3
+ */
4
+
5
+ export { hardcodedRegionRule } from "./hardcoded-region";
6
+ export { s3EncryptionRule } from "./s3-encryption";
7
+ export { iamWildcardRule } from "./iam-wildcard";