/** * WAW069: Template references an undeclared condition * * A resource's `Condition` key, an output's `Condition` key, `Fn::If`, and * the `{ "Condition": "" }` reference form inside the `Conditions` * section all name a condition that must be declared in the template's * `Conditions` section — CloudFormation rejects the template otherwise * (cfn-lint E8002/E1028). chant can build such a template when a source * names a condition that was never declared (#2068); this check turns that * deploy-time rejection into a build error. */ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth"; import { parseCFTemplate, type CFTemplate } from "./cf-refs"; interface ConditionRef { name: string; via: string; } /** Collect every `Fn::If` condition name nested anywhere inside a value. */ function collectFnIfRefs(value: unknown, via: string, out: ConditionRef[], seen = new Set()): void { if (value === null || typeof value !== "object" || seen.has(value)) return; seen.add(value); if (Array.isArray(value)) { for (const item of value) collectFnIfRefs(item, via, out, seen); return; } const obj = value as Record; const fnIf = obj["Fn::If"]; if (Array.isArray(fnIf) && typeof fnIf[0] === "string") { out.push({ name: fnIf[0], via: `${via} (Fn::If)` }); } for (const child of Object.values(obj)) collectFnIfRefs(child, via, out, seen); } /** Collect `{ "Condition": "" }` references inside a condition expression. */ function collectConditionRefs(value: unknown, via: string, out: ConditionRef[], seen = new Set()): void { if (value === null || typeof value !== "object" || seen.has(value)) return; seen.add(value); if (Array.isArray(value)) { for (const item of value) collectConditionRefs(item, via, out, seen); return; } const obj = value as Record; if (typeof obj.Condition === "string" && Object.keys(obj).length === 1) { out.push({ name: obj.Condition, via: `${via} (Condition reference)` }); } for (const child of Object.values(obj)) collectConditionRefs(child, via, out, seen); } export function checkUndeclaredConditions(ctx: PostSynthContext): PostSynthDiagnostic[] { const diagnostics: PostSynthDiagnostic[] = []; for (const [_lexicon, output] of ctx.outputs) { const template: CFTemplate | null = parseCFTemplate(output); if (!template?.Resources) continue; const conditions = (template.Conditions ?? {}) as Record; const declared = new Set(Object.keys(conditions)); const refs: ConditionRef[] = []; for (const [logicalId, resource] of Object.entries(template.Resources)) { if (typeof resource.Condition === "string") { refs.push({ name: resource.Condition, via: `resource "${logicalId}"` }); } collectFnIfRefs(resource.Properties, `resource "${logicalId}"`, refs); } const outputs = (template.Outputs ?? {}) as Record>; for (const [name, out] of Object.entries(outputs)) { if (typeof out?.Condition === "string") { refs.push({ name: out.Condition, via: `output "${name}"` }); } collectFnIfRefs(out?.Value, `output "${name}"`, refs); } for (const [name, expression] of Object.entries(conditions)) { collectConditionRefs(expression, `condition "${name}"`, refs); collectFnIfRefs(expression, `condition "${name}"`, refs); } const reported = new Set(); for (const ref of refs) { if (declared.has(ref.name)) continue; const key = `${ref.name}${ref.via}`; if (reported.has(key)) continue; reported.add(key); diagnostics.push({ checkId: "WAW069", severity: "error", message: `${ref.via} references condition "${ref.name}" but the template declares no such condition — CloudFormation will reject this template; declare it with \`new Condition(...)\` (or fix the name)`, entity: ref.via.match(/"([^"]+)"/)?.[1] ?? ref.name, lexicon: "aws", }); } } return diagnostics; } export const waw069: PostSynthCheck = { id: "WAW069", description: "Template references a condition it never declares — resource/output Condition keys, Fn::If, and Condition references must name an entry in the Conditions section", check(ctx: PostSynthContext): PostSynthDiagnostic[] { return checkUndeclaredConditions(ctx); }, };