use crate::consts::*;
use crate::defect::ParseDefect;
use crate::ir::cfn_function_name;
use crate::ir::*;
const CONDITION_CHILDREN: &[&str] = &[FN_CONDITION, FN_EQUALS, FN_AND, FN_OR, FN_NOT];
pub fn validate_intrinsic_nesting(arena: &Arena) -> Vec<ParseDefect> {
let mut out = Vec::new();
for idx in 0..arena.len() {
let node_ref = idx as NodeRef;
let spanned = arena.get(node_ref);
let Node::Intrinsic(intrinsic) = &spanned.node else {
continue;
};
let in_rules = spanned.path.starts_with("Rules/");
for (child_ref, allowlist) in restricted_children(intrinsic, in_rules) {
if let Node::Intrinsic(child_fn) = arena.node(child_ref) {
let child_name = cfn_function_name(child_fn);
if !BOOLEAN_FN_KEYS.contains(&child_name) {
continue;
}
if !allowlist.contains(&child_name) {
let parent_name = cfn_function_name(intrinsic);
out.push(crate::make_parse_defect_at(
"E9101",
format!("'{}' is not allowed inside '{}'", child_name, parent_name),
arena.span(child_ref),
&arena.get(child_ref).path,
));
}
}
}
}
out
}
fn restricted_children(intrinsic: &IntrinsicFn, in_rules: bool) -> Vec<(NodeRef, &'static [&'static str])> {
match intrinsic {
IntrinsicFn::Equals(_, _) => {
Vec::new()
}
IntrinsicFn::And(items) => {
let allow = condition_allow(in_rules);
items.iter().map(|r| (*r, allow)).collect()
}
IntrinsicFn::Or(items) => {
let allow = condition_allow(in_rules);
items.iter().map(|r| (*r, allow)).collect()
}
IntrinsicFn::Not(child) => {
vec![(*child, condition_allow(in_rules))]
}
_ => Vec::new(),
}
}
fn condition_allow(in_rules: bool) -> &'static [&'static str] {
if in_rules { CONDITION_CHILDREN_WITH_RULES } else { CONDITION_CHILDREN }
}
const CONDITION_CHILDREN_WITH_RULES: &[&str] = &[
FN_CONDITION,
FN_EQUALS,
FN_AND,
FN_OR,
FN_NOT,
FN_CONTAINS,
FN_EACH_MEMBER_EQUALS,
FN_EACH_MEMBER_IN,
FN_REF_ALL,
FN_VALUE_OF,
FN_VALUE_OF_ALL,
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equals_operands_not_checked_by_nesting() {
let mut arena = Arena::new();
let getatt = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::GetAtt("R".into(), "Arn".into())),
span: UNKNOWN_SPAN,
path: "Resources/R/Properties/X".into(),
});
let lit = arena.alloc(SpannedNode {
node: Node::String("val".into()),
span: UNKNOWN_SPAN,
path: "Resources/R/Properties/X".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Equals(getatt, lit)),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty(), "Fn::Equals operand validity is owned by the parser check, not the nesting check");
}
#[test]
fn getatt_inside_and_is_parser_owned_not_nesting() {
let mut arena = Arena::new();
let getatt = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::GetAtt("R".into(), "Arn".into())),
span: UNKNOWN_SPAN,
path: "Conditions/C/Fn::And/0".into(),
});
let other = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Equals(getatt, getatt)),
span: UNKNOWN_SPAN,
path: "Conditions/C/Fn::And/1".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::And(vec![getatt, other])),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty(), "parser owns non-boolean operands: {:?}", diags);
}
#[test]
fn rules_section_allows_contains_in_and() {
let mut arena = Arena::new();
let list = arena.alloc(SpannedNode { node: Node::List(vec![]), span: UNKNOWN_SPAN, path: "Rules/R".into() });
let contains = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Contains(list, list)),
span: UNKNOWN_SPAN,
path: "Rules/R".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::And(vec![contains])),
span: UNKNOWN_SPAN,
path: "Rules/R".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty());
}
#[test]
fn findinmap_keys_are_not_checked_by_nesting() {
let mut arena = Arena::new();
let sub = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Sub("${x}".into(), None)),
span: UNKNOWN_SPAN,
path: "Resources/R".into(),
});
let map_name =
arena.alloc(SpannedNode { node: Node::String("M".into()), span: UNKNOWN_SPAN, path: "Resources/R".into() });
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::FindInMap(map_name, sub, sub, None)),
span: UNKNOWN_SPAN,
path: "Resources/R".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty(), "Fn::FindInMap operand validity is owned by the argument-shape check");
}
#[test]
fn condition_ref_inside_and_is_allowed() {
let mut arena = Arena::new();
let cond_ref = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Ref("Condition:IsProd".into())),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::And(vec![cond_ref])),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty());
}
#[test]
fn equals_inside_or_is_allowed() {
let mut arena = Arena::new();
let a = arena.alloc(SpannedNode {
node: Node::String("x".into()),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let eq = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Equals(a, a)),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Or(vec![eq])),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty());
}
#[test]
fn ref_inside_not_is_parser_owned_not_nesting() {
let mut arena = Arena::new();
let r = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Ref("Param".into())),
span: UNKNOWN_SPAN,
path: "Conditions/C/Fn::Not/0".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Not(r)),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty(), "parser owns non-boolean operands: {:?}", diags);
}
#[test]
fn rules_only_function_outside_rules_produces_invalid_nesting() {
let mut arena = Arena::new();
let a = arena.alloc(SpannedNode {
node: Node::String("x".into()),
span: UNKNOWN_SPAN,
path: "Conditions/C/Fn::Contains/0".into(),
});
let contains = arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Contains(a, a)),
span: UNKNOWN_SPAN,
path: "Conditions/C/Fn::And/0".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::And(vec![contains])),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert_eq!(diags.len(), 1, "{:?}", diags);
assert_eq!(diags[0].rule_id, "E9101");
}
#[test]
fn non_intrinsic_children_are_not_flagged() {
let mut arena = Arena::new();
let s = arena.alloc(SpannedNode {
node: Node::String("literal".into()),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
arena.alloc(SpannedNode {
node: Node::Intrinsic(IntrinsicFn::Equals(s, s)),
span: UNKNOWN_SPAN,
path: "Conditions/C".into(),
});
let diags = validate_intrinsic_nesting(&arena);
assert!(diags.is_empty());
}
}