nodex_core/rules/
naming.rs1use globset::Glob;
2use regex::Regex;
3use std::collections::BTreeMap;
4
5use crate::config::Config;
6use crate::model::Graph;
7
8use super::{Rule, Severity, Violation};
9
10pub struct FilenamePatternRule;
12
13impl Rule for FilenamePatternRule {
14 fn id(&self) -> &str {
15 "filename_pattern"
16 }
17
18 fn severity(&self) -> Severity {
19 Severity::Error
20 }
21
22 fn check(&self, graph: &Graph, config: &Config) -> Vec<Violation> {
23 let mut violations = Vec::new();
24
25 for rule in &config.rules.naming {
26 let Ok(glob) = Glob::new(&rule.glob) else {
27 continue;
28 };
29 let matcher = glob.compile_matcher();
30 let Ok(re) = Regex::new(&rule.pattern) else {
31 continue;
32 };
33
34 for node in graph.nodes().values() {
35 let path_str = node.path.to_string_lossy().replace('\\', "/");
36 if !matcher.is_match(&path_str) {
37 continue;
38 }
39
40 let filename = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
41
42 if !re.is_match(filename) {
43 violations.push(Violation {
44 rule_id: self.id().to_string(),
45 severity: self.severity(),
46 node_id: Some(node.id.clone()),
47 path: Some(path_str),
48 message: format!(
49 "filename {filename:?} does not match pattern {:?}",
50 rule.pattern
51 ),
52 });
53 }
54 }
55 }
56
57 violations
58 }
59}
60
61pub struct SequentialNumberingRule;
63
64impl Rule for SequentialNumberingRule {
65 fn id(&self) -> &str {
66 "sequential_numbering"
67 }
68
69 fn severity(&self) -> Severity {
70 Severity::Warning
71 }
72
73 fn check(&self, graph: &Graph, config: &Config) -> Vec<Violation> {
74 let mut violations = Vec::new();
75 let number_re = Regex::new(r"^(\d+)").expect("hardcoded regex is valid");
76
77 for rule in &config.rules.naming {
78 if !rule.sequential {
79 continue;
80 }
81
82 let Ok(glob) = Glob::new(&rule.glob) else {
83 continue;
84 };
85 let matcher = glob.compile_matcher();
86
87 let mut numbers: Vec<(u32, String)> = Vec::new();
88
89 for node in graph.nodes().values() {
90 let path_str = node.path.to_string_lossy().replace('\\', "/");
91 if !matcher.is_match(&path_str) {
92 continue;
93 }
94 let filename = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
95 if let Some(caps) = number_re.captures(filename)
96 && let Ok(n) = caps[1].parse::<u32>()
97 {
98 numbers.push((n, path_str));
99 }
100 }
101
102 numbers.sort_by_key(|(n, _)| *n);
103
104 for window in numbers.windows(2) {
105 let (prev, _) = &window[0];
106 let (curr, path) = &window[1];
107 if *curr != prev + 1 {
108 violations.push(Violation {
109 rule_id: self.id().to_string(),
110 severity: self.severity(),
111 node_id: None,
112 path: Some(path.clone()),
113 message: format!("gap in numbering: {prev} → {curr}"),
114 });
115 }
116 }
117 }
118
119 violations
120 }
121}
122
123pub struct UniqueNumberingRule;
125
126impl Rule for UniqueNumberingRule {
127 fn id(&self) -> &str {
128 "unique_numbering"
129 }
130
131 fn severity(&self) -> Severity {
132 Severity::Error
133 }
134
135 fn check(&self, graph: &Graph, config: &Config) -> Vec<Violation> {
136 let mut violations = Vec::new();
137 let number_re = Regex::new(r"^(\d+)").expect("hardcoded regex is valid");
138
139 for rule in &config.rules.naming {
140 if !rule.unique {
141 continue;
142 }
143
144 let Ok(glob) = Glob::new(&rule.glob) else {
145 continue;
146 };
147 let matcher = glob.compile_matcher();
148
149 let mut seen: BTreeMap<u32, Vec<String>> = BTreeMap::new();
150
151 for node in graph.nodes().values() {
152 let path_str = node.path.to_string_lossy().replace('\\', "/");
153 if !matcher.is_match(&path_str) {
154 continue;
155 }
156 let filename = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
157 if let Some(caps) = number_re.captures(filename)
158 && let Ok(n) = caps[1].parse::<u32>()
159 {
160 seen.entry(n).or_default().push(path_str);
161 }
162 }
163
164 for (num, paths) in &seen {
165 if paths.len() > 1 {
166 violations.push(Violation {
167 rule_id: self.id().to_string(),
168 severity: self.severity(),
169 node_id: None,
170 path: Some(paths[0].clone()),
171 message: format!("duplicate number {num} in files: {}", paths.join(", ")),
172 });
173 }
174 }
175 }
176
177 violations
178 }
179}