@intentius/chant-lexicon-aws 0.0.8 → 0.0.10
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 +5743 -896
- package/dist/rules/cf-refs.ts +99 -0
- package/dist/rules/ext001.ts +30 -21
- package/dist/rules/hardcoded-region.ts +1 -0
- package/dist/rules/iam-wildcard.ts +1 -0
- package/dist/rules/s3-encryption.ts +1 -0
- 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 +388 -30
- package/dist/types/index.d.ts +1552 -1528
- 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 +2 -2
- package/src/codegen/docs-links.test.ts +143 -0
- package/src/codegen/docs.ts +247 -132
- package/src/codegen/generate-lexicon.ts +8 -0
- package/src/codegen/generate-typescript.ts +25 -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 +1552 -1528
- package/src/generated/lexicon-aws.json +5743 -896
- package/src/import/roundtrip-fixtures.test.ts +1 -1
- package/src/index.ts +21 -0
- package/src/integration.test.ts +71 -0
- 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 +30 -21
- 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/s3-encryption.ts +1 -0
- package/src/lsp/hover.ts +15 -0
- package/src/nested-stack-integration.test.ts +100 -0
- package/src/nested-stack.ts +1 -1
- package/src/plugin.ts +468 -36
- package/src/serializer.test.ts +330 -2
- package/src/serializer.ts +62 -1
- 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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
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 { waw015 } from "./waw015";
|
|
7
|
+
|
|
8
|
+
function makeChildProject(
|
|
9
|
+
projectPath: string,
|
|
10
|
+
childEntities?: Map<string, Declarable>,
|
|
11
|
+
): ChildProjectInstance {
|
|
12
|
+
return {
|
|
13
|
+
[CHILD_PROJECT_MARKER]: true,
|
|
14
|
+
[DECLARABLE_MARKER]: true,
|
|
15
|
+
lexicon: "aws",
|
|
16
|
+
entityType: "AWS::CloudFormation::Stack",
|
|
17
|
+
kind: "resource",
|
|
18
|
+
projectPath,
|
|
19
|
+
logicalName: "Child",
|
|
20
|
+
outputs: {},
|
|
21
|
+
options: {},
|
|
22
|
+
...(childEntities && {
|
|
23
|
+
buildResult: {
|
|
24
|
+
outputs: new Map(),
|
|
25
|
+
entities: childEntities,
|
|
26
|
+
warnings: [],
|
|
27
|
+
errors: [],
|
|
28
|
+
sourceFileCount: 1,
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeCtx(entities: Map<string, Declarable>): PostSynthContext {
|
|
35
|
+
return {
|
|
36
|
+
outputs: new Map(),
|
|
37
|
+
entities,
|
|
38
|
+
buildResult: {
|
|
39
|
+
outputs: new Map(),
|
|
40
|
+
entities,
|
|
41
|
+
warnings: [],
|
|
42
|
+
errors: [],
|
|
43
|
+
sourceFileCount: 1,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("WAW015: Circular Project References", () => {
|
|
49
|
+
test("check metadata", () => {
|
|
50
|
+
expect(waw015.id).toBe("WAW015");
|
|
51
|
+
expect(waw015.description).toContain("Circular");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("detects simple two-node cycle (A → B → A)", () => {
|
|
55
|
+
// A's child entities include a child project pointing to B's path
|
|
56
|
+
// B's child entities include a child project pointing to A's path
|
|
57
|
+
const bInA = makeChildProject("/tmp/B");
|
|
58
|
+
const aInB = makeChildProject("/tmp/A");
|
|
59
|
+
|
|
60
|
+
const childA = makeChildProject("/tmp/A", new Map<string, Declarable>([
|
|
61
|
+
["B", bInA],
|
|
62
|
+
]));
|
|
63
|
+
const childB = makeChildProject("/tmp/B", new Map<string, Declarable>([
|
|
64
|
+
["A", aInB],
|
|
65
|
+
]));
|
|
66
|
+
|
|
67
|
+
const entities = new Map<string, Declarable>([
|
|
68
|
+
["A", childA],
|
|
69
|
+
["B", childB],
|
|
70
|
+
]);
|
|
71
|
+
const ctx = makeCtx(entities);
|
|
72
|
+
const diags = waw015.check(ctx);
|
|
73
|
+
expect(diags).toHaveLength(1);
|
|
74
|
+
expect(diags[0].checkId).toBe("WAW015");
|
|
75
|
+
expect(diags[0].severity).toBe("error");
|
|
76
|
+
expect(diags[0].message).toContain("Circular");
|
|
77
|
+
expect(diags[0].message).toContain("A");
|
|
78
|
+
expect(diags[0].message).toContain("B");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("detects three-node cycle (A → B → C → A)", () => {
|
|
82
|
+
const childA = makeChildProject("/tmp/A", new Map<string, Declarable>([
|
|
83
|
+
["B", makeChildProject("/tmp/B")],
|
|
84
|
+
]));
|
|
85
|
+
const childB = makeChildProject("/tmp/B", new Map<string, Declarable>([
|
|
86
|
+
["C", makeChildProject("/tmp/C")],
|
|
87
|
+
]));
|
|
88
|
+
const childC = makeChildProject("/tmp/C", new Map<string, Declarable>([
|
|
89
|
+
["A", makeChildProject("/tmp/A")],
|
|
90
|
+
]));
|
|
91
|
+
|
|
92
|
+
const entities = new Map<string, Declarable>([
|
|
93
|
+
["A", childA],
|
|
94
|
+
["B", childB],
|
|
95
|
+
["C", childC],
|
|
96
|
+
]);
|
|
97
|
+
const ctx = makeCtx(entities);
|
|
98
|
+
const diags = waw015.check(ctx);
|
|
99
|
+
expect(diags).toHaveLength(1);
|
|
100
|
+
expect(diags[0].message).toContain("A");
|
|
101
|
+
expect(diags[0].message).toContain("B");
|
|
102
|
+
expect(diags[0].message).toContain("C");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("no diagnostic for acyclic graph (A → B, no back-edge)", () => {
|
|
106
|
+
const childA = makeChildProject("/tmp/A", new Map<string, Declarable>([
|
|
107
|
+
["B", makeChildProject("/tmp/B")],
|
|
108
|
+
]));
|
|
109
|
+
const childB = makeChildProject("/tmp/B", new Map<string, Declarable>());
|
|
110
|
+
|
|
111
|
+
const entities = new Map<string, Declarable>([
|
|
112
|
+
["A", childA],
|
|
113
|
+
["B", childB],
|
|
114
|
+
]);
|
|
115
|
+
const ctx = makeCtx(entities);
|
|
116
|
+
const diags = waw015.check(ctx);
|
|
117
|
+
expect(diags).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("no diagnostic with fewer than 2 stacks", () => {
|
|
121
|
+
const childA = makeChildProject("/tmp/A", new Map());
|
|
122
|
+
const entities = new Map<string, Declarable>([["A", childA]]);
|
|
123
|
+
const ctx = makeCtx(entities);
|
|
124
|
+
const diags = waw015.check(ctx);
|
|
125
|
+
expect(diags).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("no diagnostic with no child projects", () => {
|
|
129
|
+
const ctx = makeCtx(new Map());
|
|
130
|
+
const diags = waw015.check(ctx);
|
|
131
|
+
expect(diags).toHaveLength(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("handles child without buildResult (not yet built)", () => {
|
|
135
|
+
// No buildResult means no edges — no cycle possible
|
|
136
|
+
const childA = makeChildProject("/tmp/A");
|
|
137
|
+
const childB = makeChildProject("/tmp/B");
|
|
138
|
+
// These have no buildResult, so deps map will be empty
|
|
139
|
+
const entities = new Map<string, Declarable>([
|
|
140
|
+
["A", childA],
|
|
141
|
+
["B", childB],
|
|
142
|
+
]);
|
|
143
|
+
const ctx = makeCtx(entities);
|
|
144
|
+
const diags = waw015.check(ctx);
|
|
145
|
+
expect(diags).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw016, checkDeprecatedProperties } from "./waw016";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Synthetic deprecated-property map — no disk dependency. */
|
|
10
|
+
function fakeDeprecated(): Map<string, Set<string>> {
|
|
11
|
+
return new Map([
|
|
12
|
+
["AWS::S3::Bucket", new Set(["AccessControl", "ObjectLockConfiguration"])],
|
|
13
|
+
["AWS::Lambda::Function", new Set(["Code"])],
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("WAW016: Deprecated Property Usage", () => {
|
|
18
|
+
test("check metadata", () => {
|
|
19
|
+
expect(waw016.id).toBe("WAW016");
|
|
20
|
+
expect(waw016.description).toContain("Deprecated");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("emits warning for deprecated property", () => {
|
|
24
|
+
const ctx = makeCtx({
|
|
25
|
+
Resources: {
|
|
26
|
+
MyBucket: {
|
|
27
|
+
Type: "AWS::S3::Bucket",
|
|
28
|
+
Properties: {
|
|
29
|
+
AccessControl: "LogDeliveryWrite",
|
|
30
|
+
BucketName: "my-bucket",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
36
|
+
expect(diags).toHaveLength(1);
|
|
37
|
+
expect(diags[0].checkId).toBe("WAW016");
|
|
38
|
+
expect(diags[0].severity).toBe("warning");
|
|
39
|
+
expect(diags[0].message).toContain("AccessControl");
|
|
40
|
+
expect(diags[0].message).toContain("MyBucket");
|
|
41
|
+
expect(diags[0].message).toContain("deprecated");
|
|
42
|
+
expect(diags[0].entity).toBe("MyBucket");
|
|
43
|
+
expect(diags[0].lexicon).toBe("aws");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("emits one warning per deprecated property", () => {
|
|
47
|
+
const ctx = makeCtx({
|
|
48
|
+
Resources: {
|
|
49
|
+
MyBucket: {
|
|
50
|
+
Type: "AWS::S3::Bucket",
|
|
51
|
+
Properties: {
|
|
52
|
+
AccessControl: "Private",
|
|
53
|
+
ObjectLockConfiguration: {},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
59
|
+
expect(diags).toHaveLength(2);
|
|
60
|
+
const props = diags.map((d) => d.message);
|
|
61
|
+
expect(props.some((m) => m.includes("AccessControl"))).toBe(true);
|
|
62
|
+
expect(props.some((m) => m.includes("ObjectLockConfiguration"))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("no diagnostic for non-deprecated properties", () => {
|
|
66
|
+
const ctx = makeCtx({
|
|
67
|
+
Resources: {
|
|
68
|
+
MyBucket: {
|
|
69
|
+
Type: "AWS::S3::Bucket",
|
|
70
|
+
Properties: {
|
|
71
|
+
BucketName: "clean-bucket",
|
|
72
|
+
VersioningConfiguration: { Status: "Enabled" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
78
|
+
expect(diags).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("no diagnostic for resource type not in map", () => {
|
|
82
|
+
const ctx = makeCtx({
|
|
83
|
+
Resources: {
|
|
84
|
+
MyRole: {
|
|
85
|
+
Type: "AWS::IAM::Role",
|
|
86
|
+
Properties: { RoleName: "test" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
91
|
+
expect(diags).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("no diagnostic on empty template", () => {
|
|
95
|
+
const ctx = makeCtx({ Resources: {} });
|
|
96
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
97
|
+
expect(diags).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("handles resource with no Properties", () => {
|
|
101
|
+
const ctx = makeCtx({
|
|
102
|
+
Resources: {
|
|
103
|
+
MyBucket: { Type: "AWS::S3::Bucket" },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
107
|
+
expect(diags).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns empty when deprecated map is empty", () => {
|
|
111
|
+
const ctx = makeCtx({
|
|
112
|
+
Resources: {
|
|
113
|
+
MyBucket: {
|
|
114
|
+
Type: "AWS::S3::Bucket",
|
|
115
|
+
Properties: { AccessControl: "Private" },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const diags = checkDeprecatedProperties(ctx, new Map());
|
|
120
|
+
expect(diags).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("flags deprecated properties across multiple resources", () => {
|
|
124
|
+
const ctx = makeCtx({
|
|
125
|
+
Resources: {
|
|
126
|
+
Bucket: {
|
|
127
|
+
Type: "AWS::S3::Bucket",
|
|
128
|
+
Properties: { AccessControl: "Private" },
|
|
129
|
+
},
|
|
130
|
+
Func: {
|
|
131
|
+
Type: "AWS::Lambda::Function",
|
|
132
|
+
Properties: { Code: {} },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
137
|
+
expect(diags).toHaveLength(2);
|
|
138
|
+
expect(diags[0].entity).toBe("Bucket");
|
|
139
|
+
expect(diags[1].entity).toBe("Func");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW016: Deprecated Property Usage
|
|
3
|
+
*
|
|
4
|
+
* Flags properties marked as deprecated in the CloudFormation Registry.
|
|
5
|
+
* Sources: explicit `deprecatedProperties` array + description text mining.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
12
|
+
|
|
13
|
+
interface LexiconEntry {
|
|
14
|
+
kind: string;
|
|
15
|
+
resourceType: string;
|
|
16
|
+
deprecatedProperties?: string[];
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load deprecated properties per resource type from the lexicon JSON.
|
|
22
|
+
*/
|
|
23
|
+
function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
24
|
+
const map = new Map<string, Set<string>>();
|
|
25
|
+
try {
|
|
26
|
+
const pkgDir = join(__dirname, "..", "..", "..");
|
|
27
|
+
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
28
|
+
const content = readFileSync(lexiconPath, "utf-8");
|
|
29
|
+
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
30
|
+
|
|
31
|
+
for (const [_name, entry] of Object.entries(data)) {
|
|
32
|
+
if (entry.kind === "resource" && entry.resourceType && entry.deprecatedProperties && entry.deprecatedProperties.length > 0) {
|
|
33
|
+
map.set(entry.resourceType, new Set(entry.deprecatedProperties));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Lexicon not available — skip
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
44
|
+
*/
|
|
45
|
+
export function checkDeprecatedProperties(
|
|
46
|
+
ctx: PostSynthContext,
|
|
47
|
+
deprecated: Map<string, Set<string>>,
|
|
48
|
+
): PostSynthDiagnostic[] {
|
|
49
|
+
if (deprecated.size === 0) return [];
|
|
50
|
+
|
|
51
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
52
|
+
|
|
53
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
54
|
+
const template = parseCFTemplate(output);
|
|
55
|
+
if (!template?.Resources) continue;
|
|
56
|
+
|
|
57
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
58
|
+
const deprProps = deprecated.get(resource.Type);
|
|
59
|
+
if (!deprProps) continue;
|
|
60
|
+
|
|
61
|
+
const props = resource.Properties ?? {};
|
|
62
|
+
for (const propName of Object.keys(props)) {
|
|
63
|
+
if (deprProps.has(propName)) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
checkId: "WAW016",
|
|
66
|
+
severity: "warning",
|
|
67
|
+
message: `Resource "${logicalId}" (${resource.Type}) uses deprecated property "${propName}" — consider alternatives`,
|
|
68
|
+
entity: logicalId,
|
|
69
|
+
lexicon: "aws",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return diagnostics;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const waw016: PostSynthCheck = {
|
|
80
|
+
id: "WAW016",
|
|
81
|
+
description: "Deprecated property usage — flags properties marked as deprecated in the CloudFormation Registry",
|
|
82
|
+
|
|
83
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
84
|
+
return checkDeprecatedProperties(ctx, loadDeprecatedProperties());
|
|
85
|
+
},
|
|
86
|
+
};
|