use crate::conditions::{ConditionModel, Satisfiability};
use crate::consts::*;
use crate::defect::{DefectPhase, ParseDefect};
use crate::ir::*;
use std::collections::HashMap;
fn is_foreach_key(key: &str) -> bool {
let Some(name) = key.strip_prefix(FN_FOR_EACH_KEY_PREFIX) else {
return false;
};
!name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric())
}
#[derive(Clone, Copy)]
struct SlotPermits {
length: bool,
to_json_string: bool,
}
impl SlotPermits {
const OPEN: SlotPermits = SlotPermits { length: true, to_json_string: true };
const NONE: SlotPermits = SlotPermits { length: false, to_json_string: false };
fn permits(&self, name: &str) -> bool {
match name {
FN_LENGTH => self.length,
FN_TO_JSON_STRING => self.to_json_string,
_ => false,
}
}
}
pub fn validate_language_extensions(arena: &Arena, transforms: &[String]) -> Vec<ParseDefect> {
if transforms.iter().any(|t| t == TRANSFORM_LANGUAGE_EXTENSIONS) {
return Vec::new();
}
let mut out = Vec::new();
collect_foreach_keys(arena, &mut out);
collect_length_and_to_json(arena, &mut out);
collect_findinmap_default_value(arena, &mut out);
out
}
pub fn validate_lifecycle_intrinsics(
arena: &Arena,
conditions: &ConditionModel,
lifecycle_attribute_nodes: &HashMap<(String, String), NodeRef>,
resource_conditions: &HashMap<String, String>,
) -> Vec<ParseDefect> {
let mut roots: Vec<_> = lifecycle_attribute_nodes.iter().collect();
roots.sort_unstable_by_key(|(_, node_ref)| **node_ref);
let mut out = Vec::new();
for ((resource_id, attribute), node_ref) in roots {
if attribute != KEY_CREATION_POLICY {
continue;
}
let mut assumptions =
resource_conditions.get(resource_id).map(|condition| vec![(condition.clone(), true)]).unwrap_or_default();
if matches!(conditions.satisfiability(&assumptions), Satisfiability::Unsatisfiable) {
continue;
}
let node_ref = *node_ref;
let Node::Intrinsic(intrinsic) = arena.node(node_ref) else {
continue;
};
match intrinsic {
IntrinsicFn::If(condition, when_true, when_false) => validate_creation_policy_if(
arena,
conditions,
Some(condition),
*when_true,
*when_false,
&mut assumptions,
&mut out,
),
IntrinsicFn::IfExpr(_, when_true, when_false) => validate_creation_policy_if(
arena,
conditions,
None,
*when_true,
*when_false,
&mut assumptions,
&mut out,
),
_ => out.push(crate::make_parse_defect_at(
"F1101",
format!(
"{} is not supported as a top-level {} value; {} must be an object or {} returning an object",
cfn_function_name(intrinsic),
KEY_CREATION_POLICY,
KEY_CREATION_POLICY,
FN_IF
),
arena.span(node_ref),
&arena.get(node_ref).path,
)),
}
}
out
}
fn validate_creation_policy_if(
arena: &Arena,
conditions: &ConditionModel,
condition: Option<&str>,
when_true: NodeRef,
when_false: NodeRef,
assumptions: &mut Vec<(String, bool)>,
out: &mut Vec<ParseDefect>,
) {
for (branch, expected) in [(when_true, true), (when_false, false)] {
if let Some(condition) = condition {
assumptions.push((condition.to_string(), expected));
}
validate_creation_policy_branch(arena, conditions, branch, assumptions, out);
if condition.is_some() {
assumptions.pop();
}
}
}
fn validate_creation_policy_branch(
arena: &Arena,
conditions: &ConditionModel,
node_ref: NodeRef,
assumptions: &mut Vec<(String, bool)>,
out: &mut Vec<ParseDefect>,
) {
if matches!(conditions.satisfiability(assumptions), Satisfiability::Unsatisfiable) {
return;
}
match arena.node(node_ref) {
Node::Map(_) => {}
Node::Intrinsic(IntrinsicFn::If(condition, when_true, when_false)) => {
validate_creation_policy_if(arena, conditions, Some(condition), *when_true, *when_false, assumptions, out)
}
Node::Intrinsic(IntrinsicFn::IfExpr(_, when_true, when_false)) => {
validate_creation_policy_if(arena, conditions, None, *when_true, *when_false, assumptions, out)
}
node => out.push(crate::make_parse_defect_at(
"F1101",
format!(
"{} branch in {} must be an object, got {}",
FN_IF,
KEY_CREATION_POLICY,
creation_policy_value_shape(node)
),
arena.span(node_ref),
&arena.get(node_ref).path,
)),
}
}
fn creation_policy_value_shape(node: &Node) -> String {
match node {
Node::Null => "null".to_string(),
Node::Bool(_) => "a boolean".to_string(),
Node::Int(_) | Node::Float(_) => "a number".to_string(),
Node::String(_) => "a string".to_string(),
Node::List(_) => "a list".to_string(),
Node::Map(_) => "an object".to_string(),
Node::Intrinsic(intrinsic) => cfn_function_name(intrinsic).to_string(),
}
}
fn collect_findinmap_default_value(arena: &Arena, out: &mut Vec<ParseDefect>) {
for idx in 0..arena.len() {
let node_ref = idx as NodeRef;
if let Node::Intrinsic(IntrinsicFn::FindInMap(_, _, _, Some(_))) = arena.node(node_ref) {
out.push(crate::make_parse_defect_at(
"F1101",
format!(
"{}: the 'DefaultValue' element requires the AWS::LanguageExtensions transform; without it Fn::FindInMap accepts at most 3 elements",
FN_FIND_IN_MAP
),
arena.span(node_ref),
&arena.get(node_ref).path,
));
}
}
}
fn collect_foreach_keys(arena: &Arena, out: &mut Vec<ParseDefect>) {
for idx in 0..arena.len() {
let node_ref = idx as NodeRef;
let Node::Map(entries) = arena.node(node_ref) else {
continue;
};
for (key, value_ref) in entries {
if !is_foreach_key(key) {
continue;
}
let message = format!(
"Fn::ForEach requires the AWS::LanguageExtensions transform, but it is not declared. Add 'Transform: AWS::LanguageExtensions' to use '{}'",
key
);
let build_path = &arena.get(*value_ref).path;
let diag = if build_path == &format!("{}/{}", SECTION_RESOURCES, key) {
ParseDefect::new("F1032", message)
.location(arena.span(*value_ref))
.phase(DefectPhase::Parse)
.resource(key.clone())
} else {
crate::make_parse_defect_at("F1032", message, arena.span(*value_ref), build_path)
};
out.push(diag);
}
}
}
fn collect_length_and_to_json(arena: &Arena, out: &mut Vec<ParseDefect>) {
for idx in 0..arena.len() {
let node_ref = idx as NodeRef;
let Node::Intrinsic(intrinsic) = arena.node(node_ref) else {
continue;
};
if !is_open_value_root(&arena.get(node_ref).path) {
continue;
}
visit_value(arena, node_ref, intrinsic, SlotPermits::OPEN, out);
}
}
fn is_open_value_root(path: &str) -> bool {
let segments: Vec<&str> = path.split('/').collect();
if segments.iter().any(|s| s.starts_with(FN_PREFIX)) {
return false;
}
match segments.first().copied() {
Some(SECTION_RESOURCES) => {
segments.get(2).map(|s| *s == KEY_PROPERTIES || *s == SECTION_METADATA).unwrap_or(false)
}
Some(SECTION_OUTPUTS) => segments.get(2).map(|s| *s == KEY_VALUE).unwrap_or(false),
Some(SECTION_CONDITIONS) | Some(SECTION_RULES) => true,
_ => false,
}
}
fn visit_value(
arena: &Arena,
node_ref: NodeRef,
intrinsic: &IntrinsicFn,
permits: SlotPermits,
out: &mut Vec<ParseDefect>,
) {
match intrinsic {
IntrinsicFn::Length(inner) => {
if !permits.permits(FN_LENGTH) {
return;
}
out.push(make_transform_required(arena, node_ref, FN_LENGTH, "F1030"));
visit_child(arena, *inner, SlotPermits { length: false, to_json_string: true }, out);
}
IntrinsicFn::ToJsonString(inner) => {
if !permits.permits(FN_TO_JSON_STRING) {
return;
}
out.push(make_transform_required(arena, node_ref, FN_TO_JSON_STRING, "F1031"));
visit_child(arena, *inner, SlotPermits::OPEN, out);
}
IntrinsicFn::Base64(inner) => visit_child(arena, *inner, SlotPermits::OPEN, out),
IntrinsicFn::If(_, t, f) => {
visit_child(arena, *t, permits, out);
visit_child(arena, *f, permits, out);
}
IntrinsicFn::IfExpr(_, t, f) => {
visit_child(arena, *t, permits, out);
visit_child(arena, *f, permits, out);
}
IntrinsicFn::Equals(a, b) => {
visit_child(arena, *a, SlotPermits::OPEN, out);
visit_child(arena, *b, SlotPermits::OPEN, out);
}
IntrinsicFn::And(items) | IntrinsicFn::Or(items) => {
for item in items {
visit_child(arena, *item, SlotPermits::NONE, out);
}
}
IntrinsicFn::Not(inner) => visit_child(arena, *inner, SlotPermits::NONE, out),
IntrinsicFn::Sub(_, subs) => {
if let Some(subs) = subs {
for (_, value_ref) in subs {
visit_child(arena, *value_ref, SlotPermits { length: false, to_json_string: true }, out);
}
}
}
_ => {
for child in intrinsic_child_refs(intrinsic) {
visit_child(arena, child, SlotPermits::NONE, out);
}
}
}
}
fn visit_child(arena: &Arena, node_ref: NodeRef, permits: SlotPermits, out: &mut Vec<ParseDefect>) {
if !arena.is_valid(node_ref) {
return;
}
match arena.node(node_ref) {
Node::Intrinsic(intrinsic) => visit_value(arena, node_ref, intrinsic, permits, out),
Node::List(items) => {
for item in items {
visit_child(arena, *item, permits, out);
}
}
Node::Map(entries) => {
for (_, value_ref) in entries {
visit_child(arena, *value_ref, permits, out);
}
}
_ => {}
}
}
fn intrinsic_child_refs(intrinsic: &IntrinsicFn) -> Vec<NodeRef> {
match intrinsic {
IntrinsicFn::Join(a, b) | IntrinsicFn::Select(a, b) | IntrinsicFn::Split(a, b) => vec![*a, *b],
IntrinsicFn::FindInMap(m, k1, k2, default) => {
let mut refs = vec![*m, *k1, *k2];
refs.extend(default.iter().copied());
refs
}
IntrinsicFn::Cidr(a, b, c) => vec![*a, *b, *c],
IntrinsicFn::GetAZs(inner) | IntrinsicFn::ImportValue(inner) => vec![*inner],
IntrinsicFn::GetStackOutput(args) | IntrinsicFn::Transform(_, args) => args.iter().map(|(_, r)| *r).collect(),
IntrinsicFn::Contains(a, b) | IntrinsicFn::EachMemberEquals(a, b) | IntrinsicFn::EachMemberIn(a, b) => {
vec![*a, *b]
}
_ => Vec::new(),
}
}
fn make_transform_required(arena: &Arena, node_ref: NodeRef, fn_name: &str, rule_id: &str) -> ParseDefect {
crate::make_parse_defect_at(
rule_id,
format!(
"{} requires the AWS::LanguageExtensions transform, but it is not declared. Add 'Transform: AWS::LanguageExtensions' to use it",
fn_name
),
arena.span(node_ref),
&arena.get(node_ref).path,
)
}