bynk_project/
consistency.rs1use std::collections::{BTreeMap, HashMap};
2use std::path::{Path, PathBuf};
3
4use bynk_syntax::error::CompileError;
5
6use crate::discovery::ParsedFile;
7use crate::paths::unit_path_matches;
8use crate::roots::UnitKind;
9
10pub fn check_directory_name_consistency(
25 parsed: &[ParsedFile],
26) -> Result<(), Vec<(PathBuf, CompileError)>> {
27 let mut errors: Vec<(PathBuf, CompileError)> = Vec::new();
28 let mut by_name: HashMap<String, Vec<usize>> = HashMap::new();
32 for (i, pf) in parsed.iter().enumerate() {
33 if matches!(pf.kind, UnitKind::Test | UnitKind::Integration) {
34 continue;
35 }
36 by_name.entry(pf.unit.name().joined()).or_default().push(i);
37 }
38 for indices in by_name.values() {
39 if indices.len() < 2 {
40 continue;
41 }
42 let first_dir = parsed[indices[0]]
43 .source_path
44 .parent()
45 .unwrap_or(Path::new(""))
46 .to_path_buf();
47 for &idx in indices.iter().skip(1) {
48 let dir = parsed[idx]
49 .source_path
50 .parent()
51 .unwrap_or(Path::new(""))
52 .to_path_buf();
53 if dir != first_dir {
54 errors.push((
55 parsed[idx].identity_path.clone(),
56 CompileError::new(
57 "bynk.project.inconsistent_commons_name",
58 parsed[idx].unit.span(),
59 format!(
60 "files declaring `{}` are spread across different directories: `{}` vs `{}`",
61 parsed[idx].unit.name().joined(),
62 first_dir.display(),
63 dir.display(),
64 ),
65 )
66 .with_note(format!(
71 "the first file declaring `{}` is `{}`",
72 parsed[idx].unit.name().joined(),
73 parsed[indices[0]].identity_path.to_string_lossy().replace('\\', "/"),
74 ))
75 .with_note(
76 "all files of a multi-file commons or context must live in the same directory",
77 ),
78 ));
79 }
80 }
81 }
82 if errors.is_empty() {
83 Ok(())
84 } else {
85 Err(errors)
86 }
87}
88
89pub fn check_directory_kind_consistency(_parsed: &[ParsedFile]) -> Result<(), Vec<CompileError>> {
93 Ok(())
94}
95
96pub fn check_path_name_alignment(
101 parsed: &[ParsedFile],
102) -> Result<(), Vec<(PathBuf, CompileError)>> {
103 let mut errors: Vec<(PathBuf, CompileError)> = Vec::new();
104 for pf in parsed {
105 if matches!(pf.kind, UnitKind::Test | UnitKind::Integration) {
106 continue;
108 }
109 let name = pf.unit.name().joined();
110 let name_parts: Vec<&str> = name.split('.').collect();
111 let rel = &pf.source_path;
112 if !unit_path_matches(rel, &name) {
113 errors.push((
114 pf.identity_path.clone(),
115 CompileError::new(
116 "bynk.project.inconsistent_commons_name",
117 pf.unit.span(),
118 format!(
119 "file `{}` declares `{name}`, but its path doesn't match — expected either `{}.bynk` (single-file) or `{}/...bynk` (multi-file)",
120 rel.display(),
121 name_parts.join("/"),
122 name_parts.join("/"),
123 ),
124 )
125 .with_note(
126 "the source-tree layout determines a unit's identity: each commons or context's qualified name must match its path",
127 ),
128 ));
129 }
130 }
131 if errors.is_empty() {
132 Ok(())
133 } else {
134 Err(errors)
135 }
136}
137
138pub fn check_group_kind_consistency(
140 parsed: &[ParsedFile],
141 groups: &BTreeMap<String, Vec<usize>>,
142) -> Result<(), Vec<(PathBuf, CompileError)>> {
143 let mut errors: Vec<(PathBuf, CompileError)> = Vec::new();
144 for (name, indices) in groups {
145 if indices.len() < 2 {
146 continue;
147 }
148 let first_kind = parsed[indices[0]].kind;
149 for &idx in indices.iter().skip(1) {
150 if parsed[idx].kind != first_kind {
151 errors.push((
152 parsed[idx].identity_path.clone(),
153 CompileError::new(
154 "bynk.project.kind_conflict",
155 parsed[idx].unit.span(),
156 format!(
157 "name `{name}` is declared as both a {} and a {}",
158 first_kind.display(),
159 parsed[idx].kind.display(),
160 ),
161 )
162 .with_note(format!(
167 "first declared as a {} in `{}`",
168 first_kind.display(),
169 parsed[indices[0]]
170 .identity_path
171 .to_string_lossy()
172 .replace('\\', "/"),
173 )),
174 ));
175 }
176 }
177 }
178 if errors.is_empty() {
179 Ok(())
180 } else {
181 Err(errors)
182 }
183}
184
185pub fn check_group_module_docs(
195 parsed: &[ParsedFile],
196 groups: &BTreeMap<String, Vec<usize>>,
197) -> Result<(), Vec<(PathBuf, CompileError)>> {
198 let mut errors: Vec<(PathBuf, CompileError)> = Vec::new();
199 for (name, indices) in groups {
200 let mut documented: Vec<usize> = indices
201 .iter()
202 .copied()
203 .filter(|&i| parsed[i].unit.module_doc().is_some())
204 .collect();
205 if documented.len() < 2 {
206 continue;
207 }
208 documented.sort_by(|&a, &b| parsed[a].identity_path.cmp(&parsed[b].identity_path));
209 let files = documented
210 .iter()
211 .map(|&i| {
212 format!(
213 "`{}`",
214 parsed[i].identity_path.to_string_lossy().replace('\\', "/")
215 )
216 })
217 .collect::<Vec<_>>()
218 .join(", ");
219 for &i in &documented {
220 let pf = &parsed[i];
221 errors.push((
222 pf.identity_path.clone(),
223 CompileError::new(
224 "bynk.project.duplicate_module_doc",
225 pf.unit.name().span,
226 format!(
227 "{} `{name}` has a module doc in more than one file: {files}",
228 pf.unit.kind_name(),
229 ),
230 )
231 .with_note(
232 "a unit carries at most one module doc (the doc-block above its header); keep it in one file and turn the others into `--` comments",
233 ),
234 ));
235 }
236 }
237 if errors.is_empty() {
238 Ok(())
239 } else {
240 Err(errors)
241 }
242}