1use crate::consts::KEY_PROPERTIES;
5use crate::model::SemanticModel;
6use crate::resolver::ResolvedValue;
7use crate::value_patterns::AVAILABILITY_ZONE_PATTERN;
8use regex::Regex;
9use std::sync::LazyLock;
10
11pub const AZ_PATHS: &[(&str, &str)] = &[
18 ("AWS::AutoScaling::AutoScalingGroup", "AvailabilityZones.*"),
19 ("AWS::DAX::Cluster", "AvailabilityZones.*"),
20 ("AWS::DMS::ReplicationInstance", "AvailabilityZone"),
21 ("AWS::EC2::Host", "AvailabilityZone"),
22 ("AWS::EC2::Instance", "AvailabilityZone"),
23 ("AWS::EC2::LaunchTemplate", "LaunchTemplateData.Placement.AvailabilityZone"),
24 ("AWS::EC2::SpotFleet", "SpotFleetRequestConfigData.LaunchSpecifications.{}.Placement.AvailabilityZone"),
25 ("AWS::EC2::SpotFleet", "SpotFleetRequestConfigData.LaunchTemplateConfigs.{}.Overrides.{}.AvailabilityZone"),
26 ("AWS::EC2::Subnet", "AvailabilityZone"),
27 ("AWS::EC2::Volume", "AvailabilityZone"),
28 ("AWS::ElasticLoadBalancing::LoadBalancer", "AvailabilityZones.*"),
29 ("AWS::ElasticLoadBalancingV2::TargetGroup", "Targets.{}.AvailabilityZone"),
30 ("AWS::EMR::Cluster", "Instances.Placement.AvailabilityZone"),
31 ("AWS::Glue::Connection", "ConnectionInput.PhysicalConnectionRequirements.AvailabilityZone"),
32 ("AWS::OpsWorks::Instance", "AvailabilityZone"),
33 ("AWS::RDS::DBCluster", "AvailabilityZones.*"),
34 ("AWS::RDS::DBInstance", "AvailabilityZone"),
35];
36
37static AZ_RE: LazyLock<Regex> =
38 LazyLock::new(|| Regex::new(AVAILABILITY_ZONE_PATTERN).expect("AVAILABILITY_ZONE_PATTERN is a valid regex"));
39
40pub struct HardcodedAz {
41 pub path: String,
42 pub zone: String,
43}
44
45pub fn message(zone: &str) -> String {
46 format!("Avoid hardcoding availability zones '{zone}'")
47}
48
49pub fn find(model: &SemanticModel, resource_id: &str, resource_type: &str) -> Vec<HardcodedAz> {
52 let mut found = Vec::new();
53 for (rtype, descriptor) in AZ_PATHS {
54 if *rtype != resource_type {
55 continue;
56 }
57 let segments: Vec<&str> = descriptor.split('.').collect();
58 walk(model, resource_id, &segments, 0, KEY_PROPERTIES.to_string(), &mut found);
59 }
60 found
61}
62
63fn walk(model: &SemanticModel, name: &str, segments: &[&str], idx: usize, path: String, found: &mut Vec<HardcodedAz>) {
64 if idx == segments.len() {
65 push_if_hardcoded_az(model, name, path, found);
66 return;
67 }
68 match segments[idx] {
69 "*" => {
71 if model.is_from_intrinsic(name, &path) {
72 return;
73 }
74 let Some(len) = resolve_array_len(model, name, &path) else {
75 return;
76 };
77 for i in 0..len {
78 push_if_hardcoded_az(model, name, format!("{path}.{i}"), found);
79 }
80 }
81 "{}" => {
83 let Some(len) = resolve_array_len(model, name, &path) else {
84 return;
85 };
86 for i in 0..len {
87 walk(model, name, segments, idx + 1, format!("{path}.{i}"), found);
88 }
89 }
90 seg => walk(model, name, segments, idx + 1, format!("{path}.{seg}"), found),
91 }
92}
93
94fn push_if_hardcoded_az(model: &SemanticModel, name: &str, path: String, found: &mut Vec<HardcodedAz>) {
95 if model.is_from_intrinsic(name, &path) {
96 return;
97 }
98 if let Some(zone) = resolve_concrete_string(model, name, &path)
99 && AZ_RE.is_match(&zone)
100 {
101 found.push(HardcodedAz { path, zone });
102 }
103}
104
105fn resolve_concrete_string(model: &SemanticModel, name: &str, path: &str) -> Option<String> {
106 match model.resolve_deep(name, path).or_else(|| model.resolve(name, path).cloned())? {
107 ResolvedValue::Concrete { value } => value.as_str().map(str::to_string),
108 _ => None,
109 }
110}
111
112fn resolve_array_len(model: &SemanticModel, name: &str, path: &str) -> Option<usize> {
113 match model.resolve_deep(name, path).or_else(|| model.resolve(name, path).cloned())? {
114 ResolvedValue::Concrete { value } => value.as_array().map(Vec::len),
115 ResolvedValue::List { items } => Some(items.len()),
116 _ => None,
117 }
118}