1use anyhow::{bail, Context, Result};
14use std::env;
15use std::fs;
16use std::path::{Path, PathBuf};
17
18const LOWERING_VERSION: &str = env!("CARGO_PKG_VERSION");
22
23fn wrapper_fingerprint() -> u64 {
30 use std::sync::OnceLock;
31 static FP: OnceLock<u64> = OnceLock::new();
32 *FP.get_or_init(|| {
33 let Ok(exe) = env::current_exe() else {
34 return 0;
35 };
36 let Ok(meta) = fs::metadata(&exe) else {
37 return 0;
38 };
39 let mtime = meta
40 .modified()
41 .ok()
42 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
43 .and_then(|d| u64::try_from(d.as_nanos()).ok())
44 .unwrap_or(0);
45 meta.len() ^ mtime
46 })
47}
48
49pub fn source_cache_key(source: &str) -> u64 {
53 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
54 const FNV_PRIME: u64 = 0x100000001b3;
55 let mut hash = FNV_OFFSET;
56 for byte in LOWERING_VERSION
57 .bytes()
58 .chain(wrapper_fingerprint().to_le_bytes())
59 .chain(source.bytes())
60 {
61 hash ^= u64::from(byte);
62 hash = hash.wrapping_mul(FNV_PRIME);
63 }
64 hash
65}
66
67fn config_cache_salt(input_path: &Path) -> u64 {
74 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
75 const FNV_PRIME: u64 = 0x100000001b3;
76 let mut dir = input_path.parent();
77 while let Some(d) = dir {
78 if let Ok(text) = fs::read_to_string(d.join("trust.toml")) {
79 let mut hash = FNV_OFFSET;
80 for byte in text.bytes() {
81 hash ^= u64::from(byte);
82 hash = hash.wrapping_mul(FNV_PRIME);
83 }
84 return hash;
85 }
86 dir = d.parent();
87 }
88 0
89}
90
91pub struct Prepared {
96 pub lowered_root: PathBuf,
97 pub remap_flag: String,
98}
99
100pub fn collect_crate_callees(src_dir: &Path) -> Vec<(String, Vec<String>)> {
121 use std::collections::{HashMap, HashSet};
122 let mut sigs: HashMap<String, Vec<String>> = HashMap::new();
123 let mut ambiguous: HashSet<String> = HashSet::new();
124 let mut visited: HashSet<PathBuf> = HashSet::new();
125 collect_crate_callees_recursive(src_dir, &mut sigs, &mut ambiguous, &mut visited);
126 let mut out: Vec<(String, Vec<String>)> = sigs.into_iter().collect();
127 out.sort_by(|a, b| a.0.cmp(&b.0));
128 out
129}
130
131fn collect_crate_callees_recursive(
132 dir: &Path,
133 sigs: &mut std::collections::HashMap<String, Vec<String>>,
134 ambiguous: &mut std::collections::HashSet<String>,
135 visited: &mut std::collections::HashSet<PathBuf>,
136) {
137 if !dir.is_dir() {
138 return;
139 }
140 let Ok(read) = fs::read_dir(dir) else {
141 return;
142 };
143 for entry in read.flatten() {
144 let path = entry.path();
145 let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
146 if !visited.insert(canonical) {
147 continue;
148 }
149 if path.is_dir() {
150 collect_crate_callees_recursive(&path, sigs, ambiguous, visited);
151 } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
152 if let Ok(source) = fs::read_to_string(&path) {
153 let file = syn::parse_file(&source).ok().or_else(|| {
159 trust_lower::lower(&source)
160 .ok()
161 .and_then(|lo| syn::parse_file(&lo.source).ok())
162 });
163 if let Some(file) = file {
164 walk_items_for_sigs(&file.items, sigs, ambiguous);
165 }
166 }
167 }
168 }
169}
170
171fn walk_items_for_sigs(
172 items: &[syn::Item],
173 sigs: &mut std::collections::HashMap<String, Vec<String>>,
174 ambiguous: &mut std::collections::HashSet<String>,
175) {
176 for item in items {
177 match item {
178 syn::Item::Fn(f) => record_fn_sig(&f.sig, sigs, ambiguous),
179 syn::Item::Mod(m) => {
180 if let Some((_, inner)) = &m.content {
181 walk_items_for_sigs(inner, sigs, ambiguous);
182 }
183 }
184 _ => {}
185 }
186 }
187}
188
189fn record_fn_sig(
190 sig: &syn::Signature,
191 sigs: &mut std::collections::HashMap<String, Vec<String>>,
192 ambiguous: &mut std::collections::HashSet<String>,
193) {
194 let name = sig.ident.to_string();
195 if ambiguous.contains(&name) {
196 return;
197 }
198 let mut params: Vec<String> = Vec::new();
199 for input in &sig.inputs {
200 match input {
201 syn::FnArg::Receiver(_) => {} syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
203 syn::Pat::Ident(pi) => params.push(pi.ident.to_string()),
204 _ => {
205 sigs.remove(&name);
207 ambiguous.insert(name);
208 return;
209 }
210 },
211 }
212 }
213 match sigs.get(&name) {
214 Some(existing) if existing != ¶ms => {
215 sigs.remove(&name);
216 ambiguous.insert(name);
217 }
218 Some(_) => {}
219 None => {
220 sigs.insert(name, params);
221 }
222 }
223}
224
225pub fn find_input_rs(args: &[String]) -> Option<usize> {
230 args.iter().enumerate().find_map(|(i, a)| {
231 if a == "-" {
232 return None;
233 }
234 if a.ends_with(".rs") && !a.starts_with('-') {
235 Some(i)
236 } else {
237 None
238 }
239 })
240}
241
242pub fn crate_is_force_strict() -> bool {
252 force_strict_for(
253 env::var("TRUST_STRICT_PACKAGES").ok().as_deref(),
254 env::var("CARGO_PKG_NAME").ok().as_deref(),
255 )
256}
257
258fn force_strict_for(pkgs: Option<&str>, name: Option<&str>) -> bool {
263 let (Some(pkgs), Some(name)) = (pkgs, name) else {
264 return false;
265 };
266 let name = name.trim();
267 !name.is_empty() && pkgs.split(',').any(|p| p.trim() == name)
268}
269
270fn should_lower(source: &str) -> bool {
273 trust_lower::is_strict_source(source) || crate_is_force_strict()
274}
275
276pub fn prepare_strict_input(input_path: &Path) -> Result<Option<Prepared>> {
283 let source = match fs::read_to_string(input_path) {
284 Ok(s) => s,
285 Err(_) => return Ok(None),
286 };
287
288 if !should_lower(&source) {
289 return Ok(None);
290 }
291 let force_strict = crate_is_force_strict();
292
293 let file_name = input_path
294 .file_name()
295 .context("input path has no file name")?;
296
297 let cache_key = source_cache_key(&source) ^ config_cache_salt(input_path);
302 let cache_root = env::temp_dir().join("trust-cache");
303 let cache_dir = cache_root.join(format!("{cache_key:016x}"));
304 let cached_file = cache_dir.join(file_name);
305
306 if !cache_dir.exists() {
313 let staging = cache_root.join(format!(".staging-{cache_key:016x}-{}", std::process::id()));
314 let _ = fs::remove_dir_all(&staging);
315
316 let result = (|| -> Result<()> {
317 let src_dir = input_path
318 .parent()
319 .filter(|p| !p.as_os_str().is_empty())
320 .map(Path::to_path_buf)
321 .unwrap_or_else(|| PathBuf::from("."));
322
323 let crate_extras = collect_crate_callees(&src_dir);
328
329 let dep_extras = trust_lower::sig_index::load_from_env();
338 let extras = trust_lower::sig_index::merge(&[crate_extras, dep_extras]);
339
340 let mut visited = std::collections::HashSet::new();
341 mirror_module_tree_with_extras(&src_dir, &staging, &mut visited, &extras)
342 .with_context(|| format!("mirroring src tree from {}", src_dir.display()))?;
343
344 if !staging.join(file_name).exists() {
347 let out =
348 trust_lower::lower_with_extra_callees_forced(&source, &extras, force_strict)
349 .with_context(|| format!("lowering {}", input_path.display()))?;
350 emit_diagnostics(&out, &source, input_path)?;
351 fs::create_dir_all(&staging)?;
352 fs::write(staging.join(file_name), &out.source)?;
353 }
354 Ok(())
355 })();
356
357 if let Err(e) = result {
358 let _ = fs::remove_dir_all(&staging);
359 return Err(e);
360 }
361
362 if fs::rename(&staging, &cache_dir).is_err() {
365 let _ = fs::remove_dir_all(&staging);
366 if !cache_dir.exists() {
367 bail!(
368 "could not publish lowering cache at {}",
369 cache_dir.display()
370 );
371 }
372 }
373 }
374
375 let parent = input_path
376 .parent()
377 .filter(|p| !p.as_os_str().is_empty())
378 .map(Path::to_path_buf)
379 .unwrap_or_else(|| PathBuf::from("."));
380
381 Ok(Some(Prepared {
382 lowered_root: cached_file,
383 remap_flag: format!(
384 "--remap-path-prefix={}={}",
385 cache_dir.display(),
386 parent.display()
387 ),
388 }))
389}
390
391fn emit_diagnostics(
392 out: &trust_lower::LowerOutput,
393 original_source: &str,
394 path: &Path,
395) -> Result<()> {
396 emit_diagnostics_to(out, original_source, path, &mut std::io::stderr())
397}
398
399fn message_format_is_json() -> bool {
406 env::var("TRUST_MESSAGE_FORMAT").is_ok_and(|v| v == "json")
407}
408
409fn emit_diagnostics_to(
415 out: &trust_lower::LowerOutput,
416 original_source: &str,
417 path: &Path,
418 writer: &mut impl std::io::Write,
419) -> Result<()> {
420 let mut diagnostics = out.diagnostics.clone();
428 if out.strict_mode {
429 let file: syn::File = syn::parse_str(&out.lint_source)
433 .with_context(|| format!("re-parsing lowered source from {}", path.display()))?;
434 diagnostics.extend(trust_lints::lint_strict(&file, original_source, true).diagnostics);
435 }
436
437 let config = trust_lints::TrustConfig::discover(path)
441 .with_context(|| format!("loading trust.toml for {}", path.display()))?;
442 config.apply(&mut diagnostics);
443
444 if message_format_is_json() {
445 let name = path.display().to_string();
448 let doc = trust_diag::to_json(
449 &diagnostics,
450 trust_diag::NamedSource {
451 name: &name,
452 text: original_source,
453 },
454 );
455 write!(writer, "{doc}")?;
456 if !doc.ends_with('\n') {
457 writeln!(writer)?;
458 }
459 } else {
460 for diag in &diagnostics {
461 writeln!(
462 writer,
463 "[{}] {}: {}",
464 diag.rule,
465 if diag.is_error() { "error" } else { "warning" },
466 diag.message
467 )?;
468 }
469 }
470 if diagnostics.iter().any(|d| d.is_error()) {
471 bail!("trust check failed on {}", path.display());
472 }
473 Ok(())
474}
475
476pub fn collect_test_only_files(src_dir: &Path) -> std::collections::HashSet<PathBuf> {
491 use std::collections::HashSet;
492 let mut all_files: Vec<PathBuf> = Vec::new();
493 collect_rs_files(src_dir, &mut all_files);
494
495 let mut decls: Vec<(PathBuf, String, bool)> = Vec::new();
497 for file in &all_files {
498 let Ok(source) = fs::read_to_string(file) else {
499 continue;
500 };
501 let Ok(tokens) = source.parse::<proc_macro2::TokenStream>() else {
502 continue;
503 };
504 for (name, is_test) in file_mod_declarations(&tokens) {
505 decls.push((file.clone(), name, is_test));
506 }
507 }
508
509 let resolve = |declaring: &Path, name: &str| -> Option<PathBuf> {
510 let dir = declaring.parent()?;
511 let flat = dir.join(format!("{name}.rs"));
512 if flat.is_file() {
513 return flat.canonicalize().ok();
514 }
515 let nested = dir.join(name).join("mod.rs");
516 if nested.is_file() {
517 return nested.canonicalize().ok();
518 }
519 None
520 };
521
522 let mut test_only: HashSet<PathBuf> = HashSet::new();
523 loop {
526 let mut grew = false;
527 for (declaring, name, is_test) in &decls {
528 let from_test_file = declaring
529 .canonicalize()
530 .map(|c| test_only.contains(&c))
531 .unwrap_or(false);
532 if !is_test && !from_test_file {
533 continue;
534 }
535 if let Some(target) = resolve(declaring, name) {
536 grew |= test_only.insert(target);
537 }
538 }
539 if !grew {
540 break;
541 }
542 }
543 test_only
544}
545
546fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
547 let Ok(read) = fs::read_dir(dir) else {
548 return;
549 };
550 for entry in read.flatten() {
551 let path = entry.path();
552 if path.is_dir() {
553 collect_rs_files(&path, out);
554 } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
555 out.push(path);
556 }
557 }
558}
559
560fn cfg_args_positively_test(tokens: &proc_macro2::TokenStream) -> bool {
567 use proc_macro2::{Delimiter, TokenTree};
568 let trees: Vec<TokenTree> = tokens.clone().into_iter().collect();
569 let mut i = 0;
570 while let Some(tree) = trees.get(i) {
571 match tree {
572 TokenTree::Ident(id) if *id == "not" => {
573 if matches!(trees.get(i + 1), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis)
575 {
576 i += 2;
577 continue;
578 }
579 i += 1;
580 }
581 TokenTree::Ident(id) if *id == "any" || *id == "all" => {
582 if let Some(TokenTree::Group(g)) = trees.get(i + 1) {
583 if g.delimiter() == Delimiter::Parenthesis
584 && cfg_args_positively_test(&g.stream())
585 {
586 return true;
587 }
588 i += 2;
589 continue;
590 }
591 i += 1;
592 }
593 TokenTree::Ident(id) if *id == "test" => {
597 let followed_by_eq = matches!(
598 trees.get(i + 1),
599 Some(TokenTree::Punct(p)) if p.as_char() == '='
600 );
601 if !followed_by_eq {
602 return true;
603 }
604 i += 1;
605 }
606 _ => i += 1,
607 }
608 }
609 false
610}
611
612fn file_mod_declarations(tokens: &proc_macro2::TokenStream) -> Vec<(String, bool)> {
615 use proc_macro2::{Delimiter, TokenTree};
616 let trees: Vec<TokenTree> = tokens.clone().into_iter().collect();
617 let mut out = Vec::new();
618 let mut i = 0;
619 let mut pending_cfg_test = false;
620 while let Some(tree) = trees.get(i) {
621 match tree {
622 TokenTree::Punct(p) if p.as_char() == '#' => {
624 if let Some(TokenTree::Group(g)) = trees.get(i + 1) {
625 if g.delimiter() == Delimiter::Bracket {
626 let inner: Vec<TokenTree> = g.stream().into_iter().collect();
627 if let [TokenTree::Ident(name), TokenTree::Group(args)] = inner.as_slice() {
628 if *name == "cfg" {
629 pending_cfg_test |= cfg_args_positively_test(&args.stream());
630 }
631 }
632 i += 2;
633 continue;
634 }
635 }
636 i += 1;
637 }
638 TokenTree::Ident(id) if *id == "pub" => {
640 i += 1;
641 if let Some(TokenTree::Group(g)) = trees.get(i) {
642 if g.delimiter() == Delimiter::Parenthesis {
643 i += 1;
644 }
645 }
646 }
647 TokenTree::Ident(id) if *id == "mod" => {
648 if let (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(semi))) =
649 (trees.get(i + 1), trees.get(i + 2))
650 {
651 if semi.as_char() == ';' {
652 out.push((name.to_string(), pending_cfg_test));
653 }
654 }
655 pending_cfg_test = false;
656 i += 1;
657 }
658 _ => {
659 pending_cfg_test = false;
660 i += 1;
661 }
662 }
663 }
664 out
665}
666
667pub fn mirror_module_tree(
670 src_dir: &Path,
671 dest_dir: &Path,
672 already_done: &mut std::collections::HashSet<PathBuf>,
673) -> Result<()> {
674 mirror_module_tree_with_extras(src_dir, dest_dir, already_done, &[])
675}
676
677pub fn mirror_module_tree_with_extras(
682 src_dir: &Path,
683 dest_dir: &Path,
684 already_done: &mut std::collections::HashSet<PathBuf>,
685 extras: &[(String, Vec<String>)],
686) -> Result<()> {
687 let test_only = if crate_is_force_strict() {
691 collect_test_only_files(src_dir)
692 } else {
693 std::collections::HashSet::new()
694 };
695 mirror_inner(src_dir, dest_dir, already_done, extras, &test_only)
696}
697
698fn mirror_inner(
699 src_dir: &Path,
700 dest_dir: &Path,
701 already_done: &mut std::collections::HashSet<PathBuf>,
702 extras: &[(String, Vec<String>)],
703 test_only: &std::collections::HashSet<PathBuf>,
704) -> Result<()> {
705 if !src_dir.is_dir() {
706 return Ok(());
707 }
708 fs::create_dir_all(dest_dir).with_context(|| format!("creating {}", dest_dir.display()))?;
709
710 for entry in
711 fs::read_dir(src_dir).with_context(|| format!("reading dir {}", src_dir.display()))?
712 {
713 let entry = entry?;
714 let path = entry.path();
715 let dest = dest_dir.join(entry.file_name());
716
717 let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
718 let is_test_only = test_only.contains(&canonical);
719 if !already_done.insert(canonical) {
720 continue;
721 }
722
723 if path.is_dir() {
724 mirror_inner(&path, &dest, already_done, extras, test_only)?;
725 } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
726 let source =
727 fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
728 let lower_this = trust_lower::is_strict_source(&source)
731 || (crate_is_force_strict() && !is_test_only);
732 if lower_this {
733 let out = trust_lower::lower_with_extra_callees_forced(
734 &source,
735 extras,
736 crate_is_force_strict(),
737 )
738 .with_context(|| format!("lowering {}", path.display()))?;
739 emit_diagnostics(&out, &source, &path)?;
740 let rewritten = lower_doctests_in_source(&out.source);
747 let tmp = dest_dir.join(format!(
748 ".{}.{}.tmp",
749 entry.file_name().to_string_lossy(),
750 std::process::id()
751 ));
752 fs::write(&tmp, &rewritten)?;
753 fs::rename(&tmp, &dest)?;
754 } else {
755 fs::copy(&path, &dest).with_context(|| format!("copying {}", path.display()))?;
759 }
760 } else {
761 let _ = fs::copy(&path, &dest);
764 }
765 }
766 Ok(())
767}
768
769pub fn lower_doctests_in_source(source: &str) -> String {
792 let mut out = String::with_capacity(source.len());
793 let lines: Vec<&str> = source.lines().collect();
794 let mut i = 0;
795 while let Some(line) = lines.get(i) {
796 let (Some(prefix), Some(_)) = (doc_prefix(line), doc_body(line)) else {
797 out.push_str(line);
798 out.push('\n');
799 i += 1;
800 continue;
801 };
802 let block_start = i;
804 while lines.get(i).is_some_and(|l| doc_prefix(l) == Some(prefix)) {
805 i += 1;
806 }
807 let block_end = i;
808 let block = rewrite_doc_block(&lines[block_start..block_end], prefix);
809 out.push_str(&block);
810 }
812 out
813}
814
815fn doc_prefix(line: &str) -> Option<&'static str> {
816 let trimmed = line.trim_start();
817 if trimmed.starts_with("///") {
818 Some("///")
819 } else if trimmed.starts_with("//!") {
820 Some("//!")
821 } else {
822 None
823 }
824}
825
826fn doc_body(line: &str) -> Option<&str> {
827 let trimmed = line.trim_start();
828 let body = trimmed
829 .strip_prefix("///")
830 .or_else(|| trimmed.strip_prefix("//!"))?;
831 Some(body.strip_prefix(' ').unwrap_or(body))
832}
833
834fn rewrite_doc_block(lines: &[&str], prefix: &str) -> String {
837 let first = lines[0];
839 let indent_len = first.len().saturating_sub(first.trim_start().len());
840 let indent = &first[..indent_len];
841
842 let mut out = String::new();
846 let mut in_block = false;
847 let mut is_test_block = false;
848 let mut code_buf = String::new();
849 let mut block_indent_after_prefix = String::new();
850
851 for line in lines {
852 let body = doc_body(line).unwrap_or("");
853 let body_trim = body.trim_start();
854
855 if body_trim.starts_with("```") {
856 if !in_block {
857 let info = body_trim.trim_start_matches('`').trim();
859 is_test_block = info.is_empty()
860 || info == "rust"
861 || info.starts_with("rust,")
862 || info.starts_with("rust ");
863 in_block = true;
864 code_buf.clear();
865 block_indent_after_prefix.clear();
866 if let Some(stripped) = line.trim_start().strip_prefix(prefix) {
869 let after = stripped;
870 let extra_indent_len = after.len().saturating_sub(after.trim_start().len());
871 block_indent_after_prefix = after[..extra_indent_len].to_string();
872 }
873 out.push_str(line);
874 out.push('\n');
875 continue;
876 }
877 let lowered = if is_test_block {
879 try_lower_doctest(&code_buf).unwrap_or_else(|| code_buf.clone())
880 } else {
881 code_buf.clone()
882 };
883 for code_line in lowered.lines() {
884 out.push_str(indent);
885 out.push_str(prefix);
886 if !code_line.is_empty() {
887 if block_indent_after_prefix.is_empty() {
888 out.push(' ');
889 } else {
890 out.push_str(&block_indent_after_prefix);
891 }
892 }
893 out.push_str(code_line);
894 out.push('\n');
895 }
896 out.push_str(line);
897 out.push('\n');
898 in_block = false;
899 code_buf.clear();
900 continue;
901 }
902
903 if in_block {
904 code_buf.push_str(body);
906 code_buf.push('\n');
907 } else {
908 out.push_str(line);
909 out.push('\n');
910 }
911 }
912
913 if in_block {
915 for code_line in code_buf.lines() {
916 out.push_str(indent);
917 out.push_str(prefix);
918 out.push(' ');
919 out.push_str(code_line);
920 out.push('\n');
921 }
922 }
923 out
924}
925
926fn try_lower_doctest(snippet: &str) -> Option<String> {
930 if let Ok(out) = trust_lower::lower(snippet) {
932 if !out.diagnostics.iter().any(|d| d.is_error()) {
933 return Some(strip_hidden_doctest_prefix(out.source));
934 }
935 }
936 let wrapped = format!("fn __trust_doctest() {{\n{snippet}\n}}\n");
938 let out = trust_lower::lower(&wrapped).ok()?;
939 if out.diagnostics.iter().any(|d| d.is_error()) {
940 return None;
941 }
942 let unwrapped = unwrap_doctest_fn(&out.source)?;
947 Some(unwrapped)
948}
949
950fn unwrap_doctest_fn(source: &str) -> Option<String> {
951 let start = source.find("fn __trust_doctest()")?;
952 let open = source[start..].find('{')? + start;
953 let bytes = source.as_bytes();
955 let mut depth = 0i32;
956 let mut close = None;
957 for (i, &b) in bytes.iter().enumerate().skip(open) {
958 match b {
959 b'{' => depth += 1,
960 b'}' => {
961 depth -= 1;
962 if depth == 0 {
963 close = Some(i);
964 break;
965 }
966 }
967 _ => {}
968 }
969 }
970 let close = close?;
971 let body = &source[open + 1..close];
972 let mut lines: Vec<String> = body.lines().map(|l| l.to_string()).collect();
975 while lines.first().is_some_and(|l| l.trim().is_empty()) {
976 lines.remove(0);
977 }
978 while lines.last().is_some_and(|l| l.trim().is_empty()) {
979 lines.pop();
980 }
981 let dedent = lines
982 .iter()
983 .filter(|l| !l.trim().is_empty())
984 .map(|l| l.len().saturating_sub(l.trim_start().len()))
985 .min()
986 .unwrap_or(0);
987 let out: String = lines
988 .iter()
989 .map(|l| {
990 if l.len() >= dedent {
991 format!("{}\n", &l[dedent..])
992 } else {
993 "\n".to_string()
994 }
995 })
996 .collect();
997 Some(out)
998}
999
1000fn strip_hidden_doctest_prefix(s: String) -> String {
1006 s
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012
1013 static MESSAGE_FORMAT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1016
1017 struct MessageFormatGuard<'a> {
1021 prev: Option<String>,
1022 _lock: std::sync::MutexGuard<'a, ()>,
1023 }
1024
1025 impl MessageFormatGuard<'_> {
1026 fn set(value: Option<&str>) -> Self {
1027 let lock = MESSAGE_FORMAT_LOCK
1028 .lock()
1029 .unwrap_or_else(|poisoned| poisoned.into_inner());
1030 let prev = env::var("TRUST_MESSAGE_FORMAT").ok();
1031 match value {
1032 Some(v) => env::set_var("TRUST_MESSAGE_FORMAT", v),
1033 None => env::remove_var("TRUST_MESSAGE_FORMAT"),
1034 }
1035 MessageFormatGuard { prev, _lock: lock }
1036 }
1037 }
1038
1039 impl Drop for MessageFormatGuard<'_> {
1040 fn drop(&mut self) {
1041 match &self.prev {
1042 Some(prev) => env::set_var("TRUST_MESSAGE_FORMAT", prev),
1043 None => env::remove_var("TRUST_MESSAGE_FORMAT"),
1044 }
1045 }
1046 }
1047
1048 #[test]
1053 fn json_message_format_emits_parseable_document() {
1054 let _guard = MessageFormatGuard::set(Some("json"));
1055
1056 let source =
1057 "#![strict]\nfn main() { let v: Option<i32> = Some(1); let _ = v.unwrap(); }\n";
1058 let out = trust_lower::lower(source).expect("lowering strict source");
1059 let mut buf: Vec<u8> = Vec::new();
1060 let result = emit_diagnostics_to(&out, source, Path::new("src/main.rs"), &mut buf);
1061 assert!(result.is_err(), "R0001 is an error — must still bail");
1062
1063 let text = String::from_utf8(buf).expect("utf8 output");
1064 let doc: serde_json::Value =
1065 serde_json::from_str(text.trim()).expect("output must be valid JSON");
1066 assert_eq!(doc["file"], "src/main.rs");
1067 let rules: Vec<&str> = doc["diagnostics"]
1068 .as_array()
1069 .expect("diagnostics array")
1070 .iter()
1071 .filter_map(|d| d["rule"].as_str())
1072 .collect();
1073 assert!(rules.contains(&"R0001"), "expected R0001 in {rules:?}");
1074 }
1075
1076 #[test]
1078 fn default_message_format_is_human_lines() {
1079 let _guard = MessageFormatGuard::set(None);
1080 let source =
1081 "#![strict]\nfn main() { let v: Option<i32> = Some(1); let _ = v.unwrap(); }\n";
1082 let out = trust_lower::lower(source).expect("lowering strict source");
1083 let mut buf: Vec<u8> = Vec::new();
1084 let result = emit_diagnostics_to(&out, source, Path::new("src/main.rs"), &mut buf);
1085 assert!(result.is_err());
1086 let text = String::from_utf8(buf).expect("utf8 output");
1087 assert!(
1088 text.contains("[R0001] error:"),
1089 "expected human line, got: {text}"
1090 );
1091 }
1092
1093 #[test]
1097 fn cfg_test_mod_files_are_detected_transitively() {
1098 let base = std::env::temp_dir().join(format!("trust-rt88-{}", std::process::id()));
1099 let src = base.join("src");
1100 let _ = fs::remove_dir_all(&base);
1101 fs::create_dir_all(&src).unwrap();
1102 fs::write(
1103 src.join("main.rs"),
1104 "mod shipping;\n#[cfg(test)]\nmod tests;\nfn main() {}\n",
1105 )
1106 .unwrap();
1107 fs::write(src.join("shipping.rs"), "pub fn ship() {}\n").unwrap();
1108 fs::write(src.join("tests.rs"), "mod helpers;\nfn t() {}\n").unwrap();
1109 fs::write(src.join("helpers.rs"), "pub fn helper() {}\n").unwrap();
1110
1111 let test_only = collect_test_only_files(&src);
1112 let has = |name: &str| {
1113 test_only
1114 .iter()
1115 .any(|p| p.file_name().and_then(|f| f.to_str()) == Some(name))
1116 };
1117 assert!(has("tests.rs"), "directly cfg(test)-declared file");
1118 assert!(has("helpers.rs"), "transitively reached through tests.rs");
1119 assert!(!has("shipping.rs"), "normal mod stays enforced");
1120 assert!(!has("main.rs"), "the crate root is never test-only");
1121
1122 let _ = fs::remove_dir_all(&base);
1123 }
1124
1125 #[test]
1129 fn negated_test_cfgs_are_not_test_only() {
1130 let base = std::env::temp_dir().join(format!("trust-pr1-{}", std::process::id()));
1131 let src = base.join("src");
1132 let _ = fs::remove_dir_all(&base);
1133 fs::create_dir_all(&src).unwrap();
1134 fs::write(
1135 src.join("main.rs"),
1136 "#[cfg(not(test))]\nmod prod;\n\
1137 #[cfg(all(unix, not(test)))]\nmod prod_unix;\n\
1138 #[cfg(all(unix, test))]\nmod unix_tests;\n\
1139 #[cfg(test)]\nmod tests;\n\
1140 #[cfg(feature = \"test\")]\nmod feature_named_test;\n\
1141 fn main() {}\n",
1142 )
1143 .unwrap();
1144 for name in [
1145 "prod.rs",
1146 "prod_unix.rs",
1147 "unix_tests.rs",
1148 "tests.rs",
1149 "feature_named_test.rs",
1150 ] {
1151 fs::write(src.join(name), "pub fn x() {}\n").unwrap();
1152 }
1153
1154 let test_only = collect_test_only_files(&src);
1155 let has = |name: &str| {
1156 test_only
1157 .iter()
1158 .any(|p| p.file_name().and_then(|f| f.to_str()) == Some(name))
1159 };
1160 assert!(!has("prod.rs"), "cfg(not(test)) is a production module");
1161 assert!(!has("prod_unix.rs"), "all(unix, not(test)) is production");
1162 assert!(has("unix_tests.rs"), "all(unix, test) is test-only");
1163 assert!(has("tests.rs"), "plain cfg(test) is test-only");
1164 assert!(
1165 !has("feature_named_test.rs"),
1166 "feature = \"test\" is a feature gate, not the test predicate"
1167 );
1168
1169 let _ = fs::remove_dir_all(&base);
1170 }
1171
1172 #[test]
1175 fn force_strict_is_scoped_by_package_name() {
1176 assert!(force_strict_for(Some("my-app"), Some("my-app")));
1178 assert!(!force_strict_for(Some("my-app"), Some("serde")));
1181 assert!(force_strict_for(Some("a, b ,c"), Some("b")));
1183 assert!(!force_strict_for(None, Some("my-app")));
1185 assert!(!force_strict_for(Some("my-app"), None));
1186 assert!(!force_strict_for(Some("a,"), Some("")));
1188 }
1189
1190 #[test]
1195 fn mirror_copies_rather_than_hardlinks_source() {
1196 let base = std::env::temp_dir().join(format!("trust-rt75-{}", std::process::id()));
1197 let src = base.join("src");
1198 let dest = base.join("cache");
1199 let _ = fs::remove_dir_all(&base);
1200 fs::create_dir_all(&src).expect("create src");
1201 let src_file = src.join("plain.rs");
1202 fs::write(&src_file, "pub fn keep() {}\n").expect("write src");
1203
1204 let mut visited = std::collections::HashSet::new();
1205 mirror_module_tree(&src, &dest, &mut visited).expect("mirror");
1206
1207 fs::write(dest.join("plain.rs"), "").expect("clobber cache");
1209
1210 let after = fs::read_to_string(&src_file).expect("read src after");
1212 assert_eq!(
1213 after, "pub fn keep() {}\n",
1214 "source file was corrupted — cache shares an inode with it"
1215 );
1216 let _ = fs::remove_dir_all(&base);
1217 }
1218}