1const HASH_PREFIX: &str = "alef:hash:";
37const DEFAULT_REGENERATE_COMMAND: &str = "alef generate";
38const DEFAULT_VERIFY_COMMAND: &str = "alef verify --exit-code";
39const DEFAULT_ISSUES_URL: &str = "https://github.com/kreuzberg-dev/alef";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CommentStyle {
44 DoubleSlash,
46 Hash,
48 Block,
50}
51
52pub fn header(style: CommentStyle) -> String {
61 render_header(style, &default_header_body())
62}
63
64pub fn header_for_config(style: CommentStyle, config: &crate::config::ResolvedCrateConfig) -> String {
66 let header_config = config.scaffold.as_ref().and_then(|s| s.generated_header.as_ref());
67 let body = match header_config {
68 Some(header) => {
69 let regenerate = header
70 .regenerate_command
71 .as_deref()
72 .unwrap_or(DEFAULT_REGENERATE_COMMAND);
73 let verify = header.verify_command.as_deref().unwrap_or(DEFAULT_VERIFY_COMMAND);
74 let issues_url = header.issues_url.as_deref().unwrap_or(DEFAULT_ISSUES_URL);
75 format!(
76 "This file is auto-generated by alef — DO NOT EDIT.\n\
77To regenerate: {regenerate}\n\
78To verify freshness: {verify}\n\
79Issues & docs: {issues_url}"
80 )
81 }
82 None => default_header_body(),
83 };
84 render_header(style, &body)
85}
86
87fn default_header_body() -> String {
88 format!(
89 "This file is auto-generated by alef — DO NOT EDIT.\n\
90To regenerate: {DEFAULT_REGENERATE_COMMAND}\n\
91To verify freshness: {DEFAULT_VERIFY_COMMAND}\n\
92Issues & docs: {DEFAULT_ISSUES_URL}"
93 )
94}
95
96fn render_header(style: CommentStyle, body: &str) -> String {
97 match style {
98 CommentStyle::DoubleSlash => body.lines().map(|l| format!("// {l}\n")).collect(),
99 CommentStyle::Hash => body.lines().map(|l| format!("# {l}\n")).collect(),
100 CommentStyle::Block => {
101 let mut out = String::from("/*\n");
102 for line in body.lines() {
103 out.push_str(&format!(" * {line}\n"));
104 }
105 out.push_str(" */\n");
106 out
107 }
108 }
109}
110
111const HEADER_MARKER: &str = "auto-generated by alef";
114
115pub fn hash_content(content: &str) -> String {
121 blake3::hash(content.as_bytes()).to_hex().to_string()
122}
123
124pub fn compute_sources_hash(sources: &[std::path::PathBuf]) -> std::io::Result<String> {
138 let mut hasher = blake3::Hasher::new();
139 let mut sorted: Vec<&std::path::PathBuf> = sources.iter().collect();
140 sorted.sort();
141 for source in sorted {
142 let content = std::fs::read(source)?;
143 hasher.update(b"src\0");
144 hasher.update(source.to_string_lossy().as_bytes());
145 hasher.update(b"\0");
146 hasher.update(&content);
147 }
148 Ok(hasher.finalize().to_hex().to_string())
149}
150
151pub fn compute_crate_sources_hash(crate_cfg: &crate::config::resolved::ResolvedCrateConfig) -> std::io::Result<String> {
177 let mut all_sources: Vec<&std::path::PathBuf> = Vec::new();
178
179 for src in &crate_cfg.sources {
180 all_sources.push(src);
181 }
182 for sc in &crate_cfg.source_crates {
183 for src in &sc.sources {
184 all_sources.push(src);
185 }
186 }
187
188 all_sources.sort();
190 all_sources.dedup();
191
192 let mut hasher = blake3::Hasher::new();
193 for source in all_sources {
194 let content = std::fs::read(source)?;
195 hasher.update(b"src\0");
196 hasher.update(source.to_string_lossy().as_bytes());
197 hasher.update(b"\0");
198 hasher.update(&content);
199 }
200 Ok(hasher.finalize().to_hex().to_string())
201}
202
203pub fn compute_file_hash(sources_hash: &str, content: &str) -> String {
216 let stripped = strip_hash_line(content);
217 let mut hasher = blake3::Hasher::new();
218 hasher.update(b"sources\0");
219 hasher.update(sources_hash.as_bytes());
220 hasher.update(b"\0content\0");
221 hasher.update(stripped.as_bytes());
222 hasher.finalize().to_hex().to_string()
223}
224
225pub fn inject_hash_line(content: &str, hash: &str) -> String {
231 let mut result = String::with_capacity(content.len() + 80);
232 let mut injected = false;
233
234 for (i, line) in content.lines().enumerate() {
235 result.push_str(line);
236 result.push('\n');
237
238 if !injected && i < 10 && line.contains(HEADER_MARKER) {
239 let trimmed = line.trim();
240 let hash_line = if trimmed.starts_with("<!--") {
241 format!("<!-- {HASH_PREFIX}{hash} -->")
243 } else if trimmed.starts_with("//") {
244 format!("// {HASH_PREFIX}{hash}")
245 } else if trimmed.starts_with('#') {
246 format!("# {HASH_PREFIX}{hash}")
247 } else if trimmed.starts_with("/*") || trimmed.starts_with('*') || trimmed.ends_with("*/") {
248 format!(" * {HASH_PREFIX}{hash}")
249 } else {
250 format!("// {HASH_PREFIX}{hash}")
251 };
252 result.push_str(&hash_line);
253 result.push('\n');
254 injected = true;
255 }
256 }
257
258 if !content.ends_with('\n') && result.ends_with('\n') {
260 result.pop();
261 }
262
263 result
264}
265
266pub fn extract_hash(content: &str) -> Option<String> {
268 for (i, line) in content.lines().enumerate() {
269 if i >= 10 {
270 break;
271 }
272 if let Some(pos) = line.find(HASH_PREFIX) {
273 let rest = &line[pos + HASH_PREFIX.len()..];
274 let hex = rest.trim().trim_end_matches("*/").trim_end_matches("-->").trim();
276 if !hex.is_empty() {
277 return Some(hex.to_string());
278 }
279 }
280 }
281 None
282}
283
284pub fn strip_hash_line(content: &str) -> String {
286 let mut result = String::with_capacity(content.len());
287 for line in content.lines() {
288 if line.contains(HASH_PREFIX) {
289 continue;
290 }
291 result.push_str(line);
292 result.push('\n');
293 }
294 if !content.ends_with('\n') && result.ends_with('\n') {
296 result.pop();
297 }
298 result
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_header_double_slash() {
307 let h = header(CommentStyle::DoubleSlash);
308 assert!(h.contains("// This file is auto-generated by alef"));
309 assert!(h.contains("// Issues & docs: https://github.com/kreuzberg-dev/alef"));
310 }
311
312 #[test]
313 fn test_header_for_config_uses_configured_metadata() {
314 let cfg: crate::config::NewAlefConfig = toml::from_str(
315 r#"
316[workspace]
317languages = ["python"]
318
319[workspace.generated_header]
320issues_url = "https://docs.example.invalid/alef"
321regenerate_command = "task generate"
322verify_command = "task verify"
323
324[[crates]]
325name = "demo"
326sources = ["src/lib.rs"]
327"#,
328 )
329 .unwrap();
330 let resolved = cfg.resolve().unwrap().remove(0);
331
332 let h = header_for_config(CommentStyle::DoubleSlash, &resolved);
333
334 assert!(h.contains("// To regenerate: task generate"));
335 assert!(h.contains("// To verify freshness: task verify"));
336 assert!(h.contains("// Issues & docs: https://docs.example.invalid/alef"));
337 }
338
339 #[test]
340 fn test_header_hash() {
341 let h = header(CommentStyle::Hash);
342 assert!(h.contains("# This file is auto-generated by alef"));
343 }
344
345 #[test]
346 fn test_header_block() {
347 let h = header(CommentStyle::Block);
348 assert!(h.starts_with("/*\n"));
349 assert!(h.contains(" * This file is auto-generated by alef"));
350 assert!(h.ends_with(" */\n"));
351 }
352
353 #[test]
354 fn test_inject_and_extract_rust() {
355 let h = header(CommentStyle::DoubleSlash);
356 let content = format!("{h}use foo;\n");
357 let hash = hash_content(&content);
358 let injected = inject_hash_line(&content, &hash);
359 assert!(injected.contains(HASH_PREFIX));
360 assert_eq!(extract_hash(&injected), Some(hash));
361 }
362
363 #[test]
364 fn test_inject_and_extract_python() {
365 let h = header(CommentStyle::Hash);
366 let content = format!("{h}import foo\n");
367 let hash = hash_content(&content);
368 let injected = inject_hash_line(&content, &hash);
369 assert!(injected.contains(&format!("# {HASH_PREFIX}")));
370 assert_eq!(extract_hash(&injected), Some(hash));
371 }
372
373 #[test]
374 fn test_inject_and_extract_c_block() {
375 let h = header(CommentStyle::Block);
376 let content = format!("{h}#include <stdio.h>\n");
377 let hash = hash_content(&content);
378 let injected = inject_hash_line(&content, &hash);
379 assert!(injected.contains(HASH_PREFIX));
380 assert!(
384 injected.contains(&format!(" * {HASH_PREFIX}")),
385 "expected ' * {HASH_PREFIX}' in block-comment header, got:\n{injected}"
386 );
387 assert!(
388 !injected.contains(&format!("// {HASH_PREFIX}")),
389 "block-comment header must not use '//' for the hash line, got:\n{injected}"
390 );
391 assert_eq!(extract_hash(&injected), Some(hash));
392 }
393
394 #[test]
395 fn test_inject_php_line2() {
396 let h = header(CommentStyle::DoubleSlash);
397 let content = format!("<?php\n{h}namespace Foo;\n");
398 let hash = hash_content(&content);
399 let injected = inject_hash_line(&content, &hash);
400 let lines: Vec<&str> = injected.lines().collect();
401 assert_eq!(lines[0], "<?php");
402 assert!(lines[1].contains(HEADER_MARKER));
403 assert!(lines.iter().any(|l| l.contains(HASH_PREFIX)));
404 assert_eq!(extract_hash(&injected), Some(hash));
405 }
406
407 #[test]
408 fn test_no_header_returns_unchanged() {
409 let content = "fn main() {}\n";
410 let injected = inject_hash_line(content, "abc123");
411 assert_eq!(injected, content);
412 assert_eq!(extract_hash(&injected), None);
413 }
414
415 #[test]
416 fn test_strip_hash_line() {
417 let content = "// auto-generated by alef\n// alef:hash:abc123\nuse foo;\n";
418 let stripped = strip_hash_line(content);
419 assert_eq!(stripped, "// auto-generated by alef\nuse foo;\n");
420 }
421
422 #[test]
423 fn test_roundtrip() {
424 let h = header(CommentStyle::Hash);
425 let original = format!("{h}import sys\n");
426 let hash = hash_content(&original);
427 let injected = inject_hash_line(&original, &hash);
428 let stripped = strip_hash_line(&injected);
429 assert_eq!(stripped, original);
430 assert_eq!(hash_content(&stripped), hash);
431 }
432
433 use std::path::{Path, PathBuf};
436 use tempfile::tempdir;
437
438 fn write_file(dir: &Path, name: &str, content: &str) -> PathBuf {
439 let path = dir.join(name);
440 std::fs::write(&path, content).unwrap();
441 path
442 }
443
444 #[test]
445 fn sources_hash_changes_when_path_changes_even_if_content_same() {
446 let dir = tempdir().unwrap();
447 let s_a = write_file(dir.path(), "a.rs", "fn a() {}");
448 std::fs::create_dir_all(dir.path().join("moved")).unwrap();
449 let s_b = write_file(dir.path(), "moved/a.rs", "fn a() {}");
450 let h_a = compute_sources_hash(&[s_a]).unwrap();
451 let h_b = compute_sources_hash(&[s_b]).unwrap();
452 assert_ne!(
453 h_a, h_b,
454 "same content at a different path can produce different IR (rust_path differs)"
455 );
456 }
457
458 #[test]
459 fn sources_hash_errors_on_missing_source() {
460 let dir = tempdir().unwrap();
461 let bogus = dir.path().join("does-not-exist.rs");
462 assert!(compute_sources_hash(&[bogus]).is_err());
463 }
464
465 #[test]
466 fn sources_hash_stable_across_runs() {
467 let dir = tempdir().unwrap();
468 let s1 = write_file(dir.path(), "a.rs", "fn a() {}");
469 let s2 = write_file(dir.path(), "b.rs", "fn b() {}");
470 let sources = vec![s1, s2];
471 let h1 = compute_sources_hash(&sources).unwrap();
472 let h2 = compute_sources_hash(&sources).unwrap();
473 assert_eq!(h1, h2);
474 }
475
476 #[test]
477 fn sources_hash_path_order_independent() {
478 let dir = tempdir().unwrap();
479 let s1 = write_file(dir.path(), "a.rs", "fn a() {}");
480 let s2 = write_file(dir.path(), "b.rs", "fn b() {}");
481 let h_forward = compute_sources_hash(&[s1.clone(), s2.clone()]).unwrap();
482 let h_reverse = compute_sources_hash(&[s2, s1]).unwrap();
483 assert_eq!(h_forward, h_reverse);
484 }
485
486 #[test]
487 fn sources_hash_changes_with_content() {
488 let dir = tempdir().unwrap();
489 let s = write_file(dir.path(), "a.rs", "fn a() {}");
490 let h_before = compute_sources_hash(std::slice::from_ref(&s)).unwrap();
491 std::fs::write(&s, "fn a() { let _ = 1; }").unwrap();
492 let h_after = compute_sources_hash(&[s]).unwrap();
493 assert_ne!(h_before, h_after);
494 }
495
496 #[test]
497 fn file_hash_idempotent_under_strip_hash_line() {
498 let sources_hash = "abc123";
501 let bare = "// auto-generated by alef\nfn body() {}\n";
502 let with_line = "// auto-generated by alef\n// alef:hash:deadbeef\nfn body() {}\n";
503
504 let h1 = compute_file_hash(sources_hash, bare);
505 let h2 = compute_file_hash(sources_hash, with_line);
506 assert_eq!(h1, h2, "hash must ignore an existing alef:hash: line");
507 }
508
509 #[test]
510 fn file_hash_changes_when_sources_change() {
511 let content = "// auto-generated by alef\nfn body() {}\n";
512 let h_a = compute_file_hash("sources_a", content);
513 let h_b = compute_file_hash("sources_b", content);
514 assert_ne!(h_a, h_b);
515 }
516
517 #[test]
518 fn file_hash_changes_when_content_changes() {
519 let sources_hash = "abc123";
520 let h_a = compute_file_hash(sources_hash, "fn a() {}\n");
521 let h_b = compute_file_hash(sources_hash, "fn b() {}\n");
522 assert_ne!(h_a, h_b);
523 }
524
525 #[test]
526 fn file_hash_independent_of_alef_version() {
527 let h = compute_file_hash("sources_hash", "fn a() {}\n");
532 assert_eq!(h.len(), 64, "blake3 hex output is 64 chars");
533 }
534
535 #[test]
536 fn crate_sources_hash_differs_across_crates_with_disjoint_sources() {
537 use crate::config::resolved::ResolvedCrateConfig;
538
539 let dir = tempdir().unwrap();
540 let a = write_file(dir.path(), "a.rs", "fn a() {}");
541 let b = write_file(dir.path(), "b.rs", "fn b() {}");
542
543 let make_cfg = |name: &str, sources: Vec<std::path::PathBuf>| ResolvedCrateConfig {
548 name: name.to_string(),
549 sources,
550 source_crates: vec![],
551 version_from: "Cargo.toml".to_string(),
552 core_import: None,
553 workspace_root: None,
554 skip_core_import: false,
555 error_type: None,
556 error_constructor: None,
557 features: vec![],
558 path_mappings: Default::default(),
559 extra_dependencies: Default::default(),
560 auto_path_mappings: true,
561 languages: vec![],
562 python: None,
563 node: None,
564 ruby: None,
565 php: None,
566 elixir: None,
567 wasm: None,
568 ffi: None,
569 go: None,
570 java: None,
571 dart: None,
572 kotlin: None,
573 kotlin_android: None,
574 jni: None,
575 swift: None,
576 gleam: None,
577 csharp: None,
578 r: None,
579 zig: None,
580 exclude: Default::default(),
581 include: Default::default(),
582 output_paths: Default::default(),
583 explicit_output: Default::default(),
584 lint: Default::default(),
585 test: Default::default(),
586 setup: Default::default(),
587 update: Default::default(),
588 clean: Default::default(),
589 build_commands: Default::default(),
590 generate: Default::default(),
591 generate_overrides: Default::default(),
592 format: Default::default(),
593 format_overrides: Default::default(),
594 dto: Default::default(),
595 tools: Default::default(),
596 opaque_types: Default::default(),
597 client_constructors: Default::default(),
598 sync: None,
599 citation: None,
600 publish: None,
601 e2e: None,
602 adapters: vec![],
603 trait_bridges: vec![],
604 scaffold: None,
605 readme: None,
606 custom_files: Default::default(),
607 custom_modules: Default::default(),
608 custom_registrations: Default::default(),
609 };
610
611 let cfg_a = make_cfg("alpha", vec![a]);
612 let cfg_b = make_cfg("beta", vec![b]);
613
614 let hash_a = compute_crate_sources_hash(&cfg_a).unwrap();
615 let hash_b = compute_crate_sources_hash(&cfg_b).unwrap();
616
617 assert_ne!(
618 hash_a, hash_b,
619 "crates with disjoint sources must produce different hashes"
620 );
621 }
622
623 #[test]
624 fn crate_sources_hash_includes_source_crates() {
625 use crate::config::{SourceCrate, resolved::ResolvedCrateConfig};
626
627 let dir = tempdir().unwrap();
628 let a = write_file(dir.path(), "a.rs", "fn a() {}");
629 let b = write_file(dir.path(), "b.rs", "fn b() {}");
630
631 let make_cfg =
632 |sources: Vec<std::path::PathBuf>, source_crate_sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
633 let source_crates = if source_crate_sources.is_empty() {
634 vec![]
635 } else {
636 vec![SourceCrate {
637 name: "extra-crate".to_string(),
638 sources: source_crate_sources,
639 }]
640 };
641 ResolvedCrateConfig {
642 name: "test".to_string(),
643 sources,
644 source_crates,
645 version_from: "Cargo.toml".to_string(),
646 core_import: None,
647 workspace_root: None,
648 skip_core_import: false,
649 error_type: None,
650 error_constructor: None,
651 features: vec![],
652 path_mappings: Default::default(),
653 extra_dependencies: Default::default(),
654 auto_path_mappings: true,
655 languages: vec![],
656 python: None,
657 node: None,
658 ruby: None,
659 php: None,
660 elixir: None,
661 wasm: None,
662 ffi: None,
663 go: None,
664 java: None,
665 dart: None,
666 kotlin: None,
667 kotlin_android: None,
668 jni: None,
669 swift: None,
670 gleam: None,
671 csharp: None,
672 r: None,
673 zig: None,
674 exclude: Default::default(),
675 include: Default::default(),
676 output_paths: Default::default(),
677 explicit_output: Default::default(),
678 lint: Default::default(),
679 test: Default::default(),
680 setup: Default::default(),
681 update: Default::default(),
682 clean: Default::default(),
683 build_commands: Default::default(),
684 generate: Default::default(),
685 generate_overrides: Default::default(),
686 format: Default::default(),
687 format_overrides: Default::default(),
688 dto: Default::default(),
689 tools: Default::default(),
690 opaque_types: Default::default(),
691 client_constructors: Default::default(),
692 sync: None,
693 citation: None,
694 publish: None,
695 e2e: None,
696 adapters: vec![],
697 trait_bridges: vec![],
698 scaffold: None,
699 readme: None,
700 custom_files: Default::default(),
701 custom_modules: Default::default(),
702 custom_registrations: Default::default(),
703 }
704 };
705
706 let cfg_without_extra = make_cfg(vec![a.clone()], vec![]);
707 let cfg_with_extra = make_cfg(vec![a.clone()], vec![b.clone()]);
708
709 let hash_without = compute_crate_sources_hash(&cfg_without_extra).unwrap();
710 let hash_with = compute_crate_sources_hash(&cfg_with_extra).unwrap();
711
712 assert_ne!(
713 hash_without, hash_with,
714 "adding a source_crate source file must change the hash"
715 );
716 }
717
718 #[test]
719 fn compute_crate_sources_hash_dedupes_overlapping_paths() {
720 use crate::config::{SourceCrate, resolved::ResolvedCrateConfig};
721 let dir = tempdir().unwrap();
725 let a = write_file(dir.path(), "a.rs", "fn a() {}");
726 let b = write_file(dir.path(), "b.rs", "fn b() {}");
727
728 let make_cfg =
729 |sources: Vec<std::path::PathBuf>, source_crate_sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
730 let source_crates = if source_crate_sources.is_empty() {
731 vec![]
732 } else {
733 vec![SourceCrate {
734 name: "extra-crate".to_string(),
735 sources: source_crate_sources,
736 }]
737 };
738 ResolvedCrateConfig {
739 name: "test".to_string(),
740 sources,
741 source_crates,
742 version_from: "Cargo.toml".to_string(),
743 core_import: None,
744 workspace_root: None,
745 skip_core_import: false,
746 error_type: None,
747 error_constructor: None,
748 features: vec![],
749 path_mappings: Default::default(),
750 extra_dependencies: Default::default(),
751 auto_path_mappings: true,
752 languages: vec![],
753 python: None,
754 node: None,
755 ruby: None,
756 php: None,
757 elixir: None,
758 wasm: None,
759 ffi: None,
760 go: None,
761 java: None,
762 dart: None,
763 kotlin: None,
764 kotlin_android: None,
765 jni: None,
766 swift: None,
767 gleam: None,
768 csharp: None,
769 r: None,
770 zig: None,
771 exclude: Default::default(),
772 include: Default::default(),
773 output_paths: Default::default(),
774 explicit_output: Default::default(),
775 lint: Default::default(),
776 test: Default::default(),
777 setup: Default::default(),
778 update: Default::default(),
779 clean: Default::default(),
780 build_commands: Default::default(),
781 generate: Default::default(),
782 generate_overrides: Default::default(),
783 format: Default::default(),
784 format_overrides: Default::default(),
785 dto: Default::default(),
786 tools: Default::default(),
787 opaque_types: Default::default(),
788 client_constructors: Default::default(),
789 sync: None,
790 citation: None,
791 publish: None,
792 e2e: None,
793 adapters: vec![],
794 trait_bridges: vec![],
795 scaffold: None,
796 readme: None,
797 custom_files: Default::default(),
798 custom_modules: Default::default(),
799 custom_registrations: Default::default(),
800 }
801 };
802
803 let cfg_with_dupes = make_cfg(vec![a.clone(), a.clone(), b.clone()], vec![a.clone()]);
805 let cfg_unique = make_cfg(vec![a.clone(), b.clone()], vec![]);
806
807 let hash_dup = compute_crate_sources_hash(&cfg_with_dupes).unwrap();
808 let hash_unique = compute_crate_sources_hash(&cfg_unique).unwrap();
809 assert_eq!(
810 hash_dup, hash_unique,
811 "duplicate source paths must not affect the per-crate sources hash"
812 );
813 }
814
815 #[test]
816 fn compute_crate_sources_hash_is_order_independent() {
817 use crate::config::resolved::ResolvedCrateConfig;
818 let dir = tempdir().unwrap();
821 let a = write_file(dir.path(), "a.rs", "fn a() {}");
822 let b = write_file(dir.path(), "b.rs", "fn b() {}");
823 let c = write_file(dir.path(), "c.rs", "fn c() {}");
824
825 let make_cfg = |sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
826 ResolvedCrateConfig {
827 name: "test".to_string(),
828 sources,
829 source_crates: vec![],
830 version_from: "Cargo.toml".to_string(),
831 core_import: None,
832 workspace_root: None,
833 skip_core_import: false,
834 error_type: None,
835 error_constructor: None,
836 features: vec![],
837 path_mappings: Default::default(),
838 extra_dependencies: Default::default(),
839 auto_path_mappings: true,
840 languages: vec![],
841 python: None,
842 node: None,
843 ruby: None,
844 php: None,
845 elixir: None,
846 wasm: None,
847 ffi: None,
848 go: None,
849 java: None,
850 dart: None,
851 kotlin: None,
852 kotlin_android: None,
853 jni: None,
854 swift: None,
855 gleam: None,
856 csharp: None,
857 r: None,
858 zig: None,
859 exclude: Default::default(),
860 include: Default::default(),
861 output_paths: Default::default(),
862 explicit_output: Default::default(),
863 lint: Default::default(),
864 test: Default::default(),
865 setup: Default::default(),
866 update: Default::default(),
867 clean: Default::default(),
868 build_commands: Default::default(),
869 generate: Default::default(),
870 generate_overrides: Default::default(),
871 format: Default::default(),
872 format_overrides: Default::default(),
873 dto: Default::default(),
874 tools: Default::default(),
875 opaque_types: Default::default(),
876 client_constructors: Default::default(),
877 sync: None,
878 citation: None,
879 publish: None,
880 e2e: None,
881 adapters: vec![],
882 trait_bridges: vec![],
883 scaffold: None,
884 readme: None,
885 custom_files: Default::default(),
886 custom_modules: Default::default(),
887 custom_registrations: Default::default(),
888 }
889 };
890
891 let cfg1 = make_cfg(vec![a.clone(), b.clone(), c.clone()]);
892 let cfg2 = make_cfg(vec![c.clone(), a.clone(), b.clone()]);
893 let cfg3 = make_cfg(vec![b.clone(), c.clone(), a.clone()]);
894
895 let h1 = compute_crate_sources_hash(&cfg1).unwrap();
896 let h2 = compute_crate_sources_hash(&cfg2).unwrap();
897 let h3 = compute_crate_sources_hash(&cfg3).unwrap();
898 assert_eq!(h1, h2, "reordering sources must not change the hash");
899 assert_eq!(h2, h3, "reordering sources must not change the hash");
900 }
901
902 #[test]
903 fn file_hash_round_trip_via_inject_extract() {
904 let sources_hash = "abc123";
908 let raw = "// auto-generated by alef\nfn body() {}\n";
909 let file_hash = compute_file_hash(sources_hash, raw);
910 let on_disk = inject_hash_line(raw, &file_hash);
911
912 let extracted = extract_hash(&on_disk).expect("hash line should be present");
913 let recomputed = compute_file_hash(sources_hash, &on_disk);
914 assert_eq!(extracted, file_hash);
915 assert_eq!(recomputed, file_hash);
916 assert_eq!(extracted, recomputed, "verify must reproduce the embedded hash");
917 }
918}