/** * WAW062: Duplicate Name Or Export Within A Template * * CloudFormation requires certain names to be unique within a single * template/stack: `Outputs.*.Export.Name`, and several resource-level * "explicit name" properties (LaunchTemplateName, AutoScalingGroupName, * GroupName, ...). Stock CDK doesn't catch a collision here — synth * succeeds and the stack fails at CloudFormation deploy/changeset time. * A duplicate is easy to introduce by copy-pasting a composite invocation * (e.g. two EcrRepository or NlbService composites) without varying the * explicit name. * * Only literal string values are compared — anything built from an * intrinsic (Fn::Sub, Ref, ...) is statically unprovable and skipped. */ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth"; import { parseCFTemplate, type CFTemplate } from "./cf-refs"; /** * CFN resource type → explicit-name property that must be unique within a * template. Deliberately narrow: only properties that collide at deploy/ * changeset time when literally duplicated, not merely "should differ" as * a style matter (that's WAW017-territory, not this rule). */ const UNIQUE_NAME_PROPS: Record = { "AWS::EC2::LaunchTemplate": "LaunchTemplateName", "AWS::EC2::SecurityGroup": "GroupName", "AWS::AutoScaling::AutoScalingGroup": "AutoScalingGroupName", "AWS::IAM::Role": "RoleName", "AWS::IAM::ManagedPolicy": "ManagedPolicyName", "AWS::S3::Bucket": "BucketName", "AWS::Lambda::Function": "FunctionName", "AWS::ECR::Repository": "RepositoryName", "AWS::DynamoDB::Table": "TableName", "AWS::ElasticLoadBalancingV2::LoadBalancer": "Name", "AWS::StepFunctions::StateMachine": "StateMachineName", }; function checkDuplicateNames(template: CFTemplate, diagnostics: PostSynthDiagnostic[]): void { const byProp = new Map>(); for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) { const prop = UNIQUE_NAME_PROPS[resource.Type]; if (!prop) continue; const value = resource.Properties?.[prop]; if (typeof value !== "string") continue; const key = `${resource.Type}${prop}`; let byValue = byProp.get(key); if (!byValue) { byValue = new Map(); byProp.set(key, byValue); } const ids = byValue.get(value) ?? []; ids.push(logicalId); byValue.set(value, ids); } for (const [key, byValue] of byProp) { const [type, prop] = key.split(""); for (const [value, ids] of byValue) { if (ids.length < 2) continue; diagnostics.push({ checkId: "WAW062", severity: "error", message: `Duplicate ${type} ${prop} "${value}" declared by ${ids.join(", ")} — CloudFormation rejects a duplicate ${prop} at deploy time`, entity: value, lexicon: "aws", }); } } } function checkDuplicateExports(template: CFTemplate, diagnostics: PostSynthDiagnostic[]): void { const byName = new Map(); for (const [outputId, output] of Object.entries((template as { Outputs?: Record }).Outputs ?? {})) { if (typeof output !== "object" || output === null) continue; const exportBlock = (output as Record).Export; if (typeof exportBlock !== "object" || exportBlock === null) continue; const name = (exportBlock as Record).Name; if (typeof name !== "string") continue; const ids = byName.get(name) ?? []; ids.push(outputId); byName.set(name, ids); } for (const [name, ids] of byName) { if (ids.length < 2) continue; diagnostics.push({ checkId: "WAW062", severity: "error", message: `Duplicate Export Name "${name}" declared by outputs ${ids.join(", ")} — CloudFormation rejects a template with duplicate export names`, entity: name, lexicon: "aws", }); } } export function checkDuplicateNamesAndExports(ctx: PostSynthContext): PostSynthDiagnostic[] { const diagnostics: PostSynthDiagnostic[] = []; for (const [_lexicon, output] of ctx.outputs) { const template = parseCFTemplate(output); if (!template?.Resources) continue; checkDuplicateNames(template, diagnostics); checkDuplicateExports(template, diagnostics); } return diagnostics; } export const waw062: PostSynthCheck = { id: "WAW062", description: "Duplicate export name or explicit resource name within a template — fails at deploy time", check(ctx: PostSynthContext): PostSynthDiagnostic[] { return checkDuplicateNamesAndExports(ctx); }, };