1use std::collections::BTreeMap;
11
12use super::build_config::BuildConfiguration;
13use super::language::{Language, LanguageSelection};
14
15pub const NORMALIZATION_VERSION: u32 = 1;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AnalysisMode {
25 Fast,
27 Structural,
29 Semantic,
31}
32
33impl AnalysisMode {
34 #[must_use]
36 pub const fn name(self) -> &'static str {
37 match self {
38 Self::Fast => "fast",
39 Self::Structural => "structural",
40 Self::Semantic => "semantic",
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct BuildVariant {
48 pub mode: AnalysisMode,
50 pub languages: LanguageSelection,
52 pub headers: Option<Language>,
57 pub normalization_version: u32,
59 pub builds: Vec<BuildConfiguration>,
72}
73
74impl BuildVariant {
75 #[must_use]
78 pub const fn fast(languages: LanguageSelection, headers: Language) -> Self {
79 Self {
80 mode: AnalysisMode::Fast,
81 languages,
82 headers: Self::headers_of(languages, headers),
83 normalization_version: NORMALIZATION_VERSION,
84 builds: Vec::new(),
85 }
86 }
87
88 #[must_use]
93 pub const fn structural(languages: LanguageSelection, headers: Language) -> Self {
94 Self {
95 mode: AnalysisMode::Structural,
96 languages,
97 headers: Self::headers_of(languages, headers),
98 normalization_version: NORMALIZATION_VERSION,
99 builds: Vec::new(),
100 }
101 }
102
103 #[must_use]
110 pub fn semantic(
111 languages: LanguageSelection,
112 headers: Language,
113 mut builds: Vec<BuildConfiguration>,
114 ) -> Self {
115 builds.sort_by_cached_key(BuildConfiguration::fingerprint);
116 Self {
117 mode: AnalysisMode::Semantic,
118 languages,
119 headers: Self::headers_of(languages, headers),
120 normalization_version: NORMALIZATION_VERSION,
121 builds,
122 }
123 }
124
125 const fn headers_of(languages: LanguageSelection, headers: Language) -> Option<Language> {
129 if languages.includes(Language::C) || languages.includes(Language::Cpp) {
130 Some(headers)
131 } else {
132 None
133 }
134 }
135
136 #[must_use]
155 pub fn canonical(&self) -> String {
156 let langs = self
157 .languages
158 .enabled()
159 .into_iter()
160 .map(Language::name)
161 .collect::<Vec<_>>()
162 .join(",");
163 let mut canonical = format!(
164 "mode={};languages={};headers={};normalization={}",
165 self.mode.name(),
166 langs,
167 self.headers.map_or("none", Language::name),
168 self.normalization_version,
169 );
170 if !self.builds.is_empty() {
171 canonical.push_str(";build=");
172 canonical.push_str(
173 &self
174 .builds
175 .iter()
176 .map(BuildConfiguration::fingerprint)
177 .collect::<Vec<_>>()
178 .join(","),
179 );
180 }
181 canonical
182 }
183
184 #[must_use]
186 pub fn fingerprint(&self) -> String {
187 blake3::hash(self.canonical().as_bytes())
188 .to_hex()
189 .to_string()
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct Partition<T> {
196 pub variant: BuildVariant,
198 pub units: Vec<T>,
200}
201
202#[must_use]
213pub fn partition<T>(
214 units: impl IntoIterator<Item = (BuildVariant, T)>,
215) -> BTreeMap<String, Partition<T>> {
216 let mut partitions: BTreeMap<String, Partition<T>> = BTreeMap::new();
217 for (variant, unit) in units {
218 partitions
219 .entry(variant.fingerprint())
220 .or_insert_with(|| Partition {
221 variant,
222 units: Vec::new(),
223 })
224 .units
225 .push(unit);
226 }
227 partitions
228}
229
230#[cfg(test)]
231mod tests {
232 use super::super::build_config::{CppBuild, RustBuild};
233 use super::*;
234
235 #[test]
236 fn fast_variant_carries_mode_and_normalization_version() {
237 let variant = BuildVariant::fast(LanguageSelection::default(), Language::C);
238 assert_eq!(variant.mode, AnalysisMode::Fast);
239 assert_eq!(variant.normalization_version, NORMALIZATION_VERSION);
240 }
241
242 #[test]
243 fn structural_variant_differs_from_fast_only_in_mode() {
244 let languages = LanguageSelection::default();
245 let fast = BuildVariant::fast(languages, Language::C);
246 let structural = BuildVariant::structural(languages, Language::C);
247 assert_eq!(structural.mode, AnalysisMode::Structural);
248 assert_eq!(structural.languages, fast.languages);
249 assert_eq!(structural.headers, fast.headers);
250 assert_eq!(structural.normalization_version, fast.normalization_version);
251 assert_ne!(fast.fingerprint(), structural.fingerprint());
253 }
254
255 #[test]
256 fn canonical_reflects_enabled_languages_in_fixed_order() {
257 let variant = BuildVariant::fast(
258 LanguageSelection {
259 rust: true,
260 c: false,
261 cpp: true,
262 },
263 Language::Cpp,
264 );
265 assert_eq!(
266 variant.canonical(),
267 "mode=fast;languages=rust,cpp;headers=cpp;normalization=1"
268 );
269 }
270
271 #[test]
272 fn distinct_variants_have_distinct_fingerprints() {
273 let all = BuildVariant::fast(LanguageSelection::default(), Language::C);
274 let rust_only = BuildVariant::fast(
275 LanguageSelection {
276 rust: true,
277 c: false,
278 cpp: false,
279 },
280 Language::C,
281 );
282 assert_ne!(all.fingerprint(), rust_only.fingerprint());
283 assert_eq!(
285 all.fingerprint(),
286 BuildVariant::fast(LanguageSelection::default(), Language::C).fingerprint()
287 );
288 }
289
290 #[test]
291 fn reading_headers_with_a_different_grammar_is_a_different_variant() {
292 let languages = LanguageSelection::default();
295 let as_c = BuildVariant::fast(languages, Language::C);
296 let as_cpp = BuildVariant::fast(languages, Language::Cpp);
297 assert_ne!(as_c, as_cpp);
298 assert_ne!(as_c.fingerprint(), as_cpp.fingerprint());
299 }
300
301 #[test]
302 fn a_run_that_enumerates_no_c_records_no_header_grammar() {
303 let rust_only = LanguageSelection {
306 rust: true,
307 c: false,
308 cpp: false,
309 };
310 let with_c = BuildVariant::fast(rust_only, Language::C);
311 let with_cpp = BuildVariant::fast(rust_only, Language::Cpp);
312 assert_eq!(with_c.headers, None);
313 assert_eq!(with_c, with_cpp);
314 assert_eq!(
315 with_c.canonical(),
316 "mode=fast;languages=rust;headers=none;normalization=1"
317 );
318 }
319
320 #[test]
324 fn a_run_that_resolved_no_build_configuration_is_identified_as_it_always_was() {
325 let variant = BuildVariant::fast(LanguageSelection::default(), Language::C);
326 assert!(variant.builds.is_empty());
327 assert!(
328 !variant.canonical().contains("build="),
329 "{}",
330 variant.canonical()
331 );
332 }
333
334 #[test]
335 fn two_builds_of_one_source_tree_are_two_variants() {
336 let languages = LanguageSelection::default();
337 let narrow = BuildVariant::semantic(
338 languages,
339 Language::Cpp,
340 vec![BuildConfiguration::Cpp(Box::new(CppBuild {
341 compiler: "clang++".into(),
342 ..CppBuild::default()
343 }))],
344 );
345 let wide = BuildVariant::semantic(
346 languages,
347 Language::Cpp,
348 vec![BuildConfiguration::Cpp(Box::new(CppBuild {
349 compiler: "clang++".into(),
350 macros: vec!["-DACCUM_WIDTH=64".into()],
351 ..CppBuild::default()
352 }))],
353 );
354 assert_ne!(narrow, wide);
355 assert_ne!(narrow.fingerprint(), wide.fingerprint());
356 }
357
358 #[test]
362 fn a_tree_answered_by_two_compilers_is_one_variant_naming_both() {
363 let languages = LanguageSelection::default();
364 let rust = BuildConfiguration::Rust(Box::new(RustBuild {
365 compiler_version: "rustc 1.85.0".into(),
366 ..RustBuild::default()
367 }));
368 let cpp = BuildConfiguration::Cpp(Box::new(CppBuild {
369 compiler: "clang++".into(),
370 ..CppBuild::default()
371 }));
372 let both =
373 BuildVariant::semantic(languages, Language::Cpp, vec![rust.clone(), cpp.clone()]);
374 let rust_only = BuildVariant::semantic(languages, Language::Cpp, vec![rust]);
375 assert_eq!(both.builds.len(), 2);
376 assert_ne!(both.fingerprint(), rust_only.fingerprint());
377 assert!(
378 both.canonical().contains(&cpp.fingerprint()),
379 "{}",
380 both.canonical()
381 );
382 }
383
384 #[test]
388 fn the_order_the_compilers_were_reached_in_is_not_part_of_the_identity() {
389 let languages = LanguageSelection::default();
390 let rust = || BuildConfiguration::Rust(Box::default());
391 let cpp = || BuildConfiguration::Cpp(Box::default());
392 let one = BuildVariant::semantic(languages, Language::Cpp, vec![rust(), cpp()]);
393 let other = BuildVariant::semantic(languages, Language::Cpp, vec![cpp(), rust()]);
394 assert_eq!(one, other);
395 assert_eq!(one.fingerprint(), other.fingerprint());
396 }
397
398 #[test]
402 fn one_language_building_differently_moves_the_whole_run() {
403 let languages = LanguageSelection::default();
404 let variant = |macros: Vec<String>| {
405 BuildVariant::semantic(
406 languages,
407 Language::Cpp,
408 vec![
409 BuildConfiguration::Rust(Box::default()),
410 BuildConfiguration::Cpp(Box::new(CppBuild {
411 compiler: "clang++".into(),
412 macros,
413 ..CppBuild::default()
414 })),
415 ],
416 )
417 };
418 assert_ne!(
419 variant(Vec::new()).fingerprint(),
420 variant(vec!["-DACCUM_WIDTH=64".into()]).fingerprint()
421 );
422 }
423
424 #[test]
428 fn resolving_one_configuration_identifies_a_run_as_it_always_did() {
429 let build = BuildConfiguration::Rust(Box::default());
430 let variant = BuildVariant::semantic(
431 LanguageSelection::default(),
432 Language::Cpp,
433 vec![build.clone()],
434 );
435 assert!(
436 variant
437 .canonical()
438 .ends_with(&format!(";build={}", build.fingerprint())),
439 "{}",
440 variant.canonical()
441 );
442 }
443
444 #[test]
448 fn a_rust_variant_and_a_cpp_variant_are_never_the_same_variant() {
449 let languages = LanguageSelection::default();
450 let rust = BuildVariant::semantic(
451 languages,
452 Language::Cpp,
453 vec![BuildConfiguration::Rust(Box::default())],
454 );
455 let cpp = BuildVariant::semantic(
456 languages,
457 Language::Cpp,
458 vec![BuildConfiguration::Cpp(Box::default())],
459 );
460 assert_ne!(rust.fingerprint(), cpp.fingerprint());
461 }
462
463 #[test]
464 fn units_are_grouped_by_the_variant_they_were_analysed_under() {
465 let languages = LanguageSelection::default();
466 let variant = |macros: Vec<String>| {
467 BuildVariant::semantic(
468 languages,
469 Language::Cpp,
470 vec![BuildConfiguration::Cpp(Box::new(CppBuild {
471 compiler: "clang++".into(),
472 macros,
473 ..CppBuild::default()
474 }))],
475 )
476 };
477 let narrow = variant(Vec::new());
478 let wide = variant(vec!["-DACCUM_WIDTH=64".into()]);
479 let partitions = partition([
480 (narrow.clone(), "narrow.cpp"),
481 (wide.clone(), "wide.cpp"),
482 (narrow.clone(), "also-narrow.cpp"),
483 ]);
484 assert_eq!(partitions.len(), 2);
485 assert_eq!(
486 partitions[&narrow.fingerprint()].units,
487 vec!["narrow.cpp", "also-narrow.cpp"]
488 );
489 assert_eq!(partitions[&wide.fingerprint()].units, vec!["wide.cpp"]);
490 }
491
492 #[test]
495 fn the_grouping_does_not_depend_on_the_order_units_arrive_in() {
496 let languages = LanguageSelection::default();
497 let fast = BuildVariant::fast(languages, Language::C);
498 let structural = BuildVariant::structural(languages, Language::C);
499 let forwards = partition([(fast.clone(), 1), (structural.clone(), 2)]);
500 let backwards = partition([(structural, 2), (fast, 1)]);
501 assert_eq!(
502 forwards.keys().collect::<Vec<_>>(),
503 backwards.keys().collect::<Vec<_>>()
504 );
505 }
506}