1mod badge;
2pub mod ci;
3pub(crate) mod codeclimate;
4mod compact;
5pub mod dupes_grouping;
6pub mod github;
7pub mod github_annotations;
8pub mod github_summary;
9pub mod grouping;
10mod human;
11mod json;
12mod markdown;
13pub(crate) mod sarif;
14mod shared;
15pub(crate) mod sink;
16mod status;
17pub(crate) mod suggestions;
18#[cfg(test)]
19pub(crate) mod test_helpers;
20
21use std::path::Path;
22use std::process::ExitCode;
23use std::time::Duration;
24
25use fallow_api::DuplicationGrouping;
26use fallow_config::{OutputFormat, RulesConfig, Severity};
27use fallow_types::duplicates::DuplicationReport;
28use fallow_types::results::AnalysisResults;
29use fallow_types::semantic::SemanticSymbolImpact;
30use fallow_types::trace::{
31 CloneTrace, DependencyTrace, ExportTrace, FileTrace, ImpactClosureTrace, PipelineTimings,
32};
33
34use crate::report::sink::outln;
35
36#[allow(
37 unused_imports,
38 reason = "used by binary crate modules (combined.rs, audit.rs)"
39)]
40pub use fallow_output::strip_root_prefix;
41pub use grouping::OwnershipResolver;
42pub(crate) use human::health::{render_health_score, render_health_trend};
43pub(crate) use status::{
44 HumanStatus, line as human_status_line, semantic_status, type_aware_meta_status,
45};
46
47pub(crate) struct WalkthroughHumanRender {
53 pub(crate) header: Vec<String>,
55 pub(crate) body: Vec<String>,
57 pub(crate) status: String,
59}
60
61#[must_use]
66pub(crate) fn walkthrough_viewed_files(
67 guide: &fallow_output::StandardWalkthroughGuide,
68 viewed: &crate::walkthrough_state::ViewedState,
69) -> Vec<String> {
70 human::walkthrough::viewed_files_for(guide, viewed)
71}
72
73#[must_use]
77pub(crate) fn build_walkthrough_human(
78 guide: &fallow_output::StandardWalkthroughGuide,
79 viewed: &crate::walkthrough_state::ViewedState,
80 show_cleared: bool,
81) -> WalkthroughHumanRender {
82 let input = human::walkthrough::WalkthroughHumanInput {
83 guide,
84 viewed,
85 show_cleared,
86 };
87 WalkthroughHumanRender {
88 header: human::walkthrough::build_focus_header(guide, viewed),
89 body: human::walkthrough::build_walkthrough_human_lines(&input),
90 status: human::walkthrough::build_status_line(guide, viewed),
91 }
92}
93
94pub(crate) struct ReportContext<'a> {
99 pub(crate) root: &'a Path,
100 pub(crate) rules: &'a RulesConfig,
101 pub(crate) elapsed: Duration,
102 pub(crate) quiet: bool,
103 pub(crate) explain: bool,
104 pub(crate) type_aware: Option<&'a fallow_types::envelope::TypeAwareMeta>,
106 pub(crate) type_aware_scope: Option<&'static str>,
109 pub(crate) group_by: Option<OwnershipResolver>,
111 pub(crate) top: Option<usize>,
113 pub(crate) summary: bool,
115 pub(crate) summary_heading: bool,
119 pub(crate) show_explain_tip: bool,
121 pub(crate) baseline_matched: Option<(usize, usize)>,
123 pub(crate) config_fixable: bool,
128 pub(crate) skip_score_and_trend: bool,
133 pub(crate) css_requested: bool,
137 pub(crate) json_style: crate::json_style::JsonStyle,
139}
140
141#[must_use]
143pub(crate) fn relative_path<'a>(path: &'a Path, root: &Path) -> &'a Path {
144 path.strip_prefix(root).unwrap_or(path)
145}
146
147#[must_use]
157pub(crate) fn format_display_path(path: &Path, root: &Path) -> String {
158 relative_path(path, root)
159 .display()
160 .to_string()
161 .replace('\\', "/")
162}
163
164#[must_use]
167pub(crate) fn split_dir_filename(path: &str) -> (&str, &str) {
168 path.rfind('/')
169 .map_or(("", path), |pos| (&path[..=pos], &path[pos + 1..]))
170}
171
172#[must_use]
174pub(crate) const fn plural(n: usize) -> &'static str {
175 if n == 1 { "" } else { "s" }
176}
177
178#[must_use]
183pub(crate) fn emit_json(value: &serde_json::Value, kind: &str) -> ExitCode {
184 match serde_json::to_string_pretty(value) {
185 Ok(json) => {
186 outln!("{json}");
187 ExitCode::SUCCESS
188 }
189 Err(e) => {
190 eprintln!("Error: failed to serialize {kind} output: {e}");
191 ExitCode::from(2)
192 }
193 }
194}
195
196#[must_use]
198pub(crate) fn emit_report_json(
199 value: &serde_json::Value,
200 kind: &str,
201 style: crate::json_style::JsonStyle,
202) -> ExitCode {
203 match style.serialize(value) {
204 Ok(json) => {
205 outln!("{json}");
206 ExitCode::SUCCESS
207 }
208 Err(e) => {
209 eprintln!("Error: failed to serialize {kind} output: {e}");
210 ExitCode::from(2)
211 }
212 }
213}
214
215#[must_use]
221pub(crate) fn elide_common_prefix<'a>(base: &str, target: &'a str) -> &'a str {
222 let mut last_sep = 0;
223 for (i, (a, b)) in base.bytes().zip(target.bytes()).enumerate() {
224 if a != b {
225 break;
226 }
227 if a == b'/' {
228 last_sep = i + 1;
229 }
230 }
231 if last_sep > 0 && last_sep <= target.len() {
232 &target[last_sep..]
233 } else {
234 target
235 }
236}
237
238#[cfg(test)]
240fn relative_uri(path: &Path, root: &Path) -> String {
241 normalize_uri(&relative_path(path, root).display().to_string())
242}
243
244#[must_use]
249pub(crate) fn normalize_uri(path_str: &str) -> String {
250 fallow_output::normalize_uri(path_str)
251}
252
253#[derive(Clone, Copy, Debug)]
255pub enum Level {
256 Warn,
257 Info,
258 Error,
259}
260
261#[must_use]
262pub(crate) const fn severity_to_level(s: Severity) -> Level {
263 match s {
264 Severity::Error => Level::Error,
265 Severity::Warn => Level::Warn,
266 Severity::Off => Level::Info,
267 }
268}
269
270#[must_use]
276pub(crate) fn print_results(
277 results: &AnalysisResults,
278 ctx: &ReportContext<'_>,
279 output: OutputFormat,
280 regression: Option<&crate::regression::RegressionOutcome>,
281) -> ExitCode {
282 if let Some(ref resolver) = ctx.group_by {
283 let groups = grouping::group_analysis_results(results, ctx.root, resolver);
284 return print_grouped_results(&groups, results, ctx, output, resolver);
285 }
286
287 match output {
288 OutputFormat::Human => {
289 if ctx.summary {
290 human::check::print_check_summary(
291 results,
292 ctx.rules,
293 ctx.elapsed,
294 ctx.quiet,
295 ctx.summary_heading,
296 );
297 } else {
298 human::print_human(&human::PrintHumanInput {
299 results,
300 root: ctx.root,
301 rules: ctx.rules,
302 elapsed: ctx.elapsed,
303 quiet: ctx.quiet,
304 top: ctx.top,
305 show_explain_tip: ctx.show_explain_tip,
306 explain: ctx.explain,
307 });
308 }
309 ExitCode::SUCCESS
310 }
311 OutputFormat::Json => json::print_json(&json::PrintJsonInput {
312 results,
313 root: ctx.root,
314 elapsed: ctx.elapsed,
315 explain: ctx.explain,
316 type_aware: ctx.type_aware,
317 regression,
318 baseline_matched: ctx.baseline_matched,
319 config_fixable: ctx.config_fixable,
320 json_style: ctx.json_style,
321 }),
322 OutputFormat::Compact => {
323 compact::print_compact(results, ctx.root);
324 compact::print_type_aware_compact(ctx.type_aware, ctx.type_aware_scope);
325 ExitCode::SUCCESS
326 }
327 OutputFormat::Sarif => sarif::print_sarif(results, ctx.root, ctx.rules, ctx.type_aware),
328 OutputFormat::Markdown => {
329 markdown::print_markdown(results, ctx.root);
330 markdown::print_type_aware_markdown(ctx.type_aware, ctx.type_aware_scope);
331 ExitCode::SUCCESS
332 }
333 OutputFormat::CodeClimate => codeclimate::print_codeclimate(results, ctx.root, ctx.rules),
334 OutputFormat::GithubAnnotations => print_check_github_annotations(results, ctx),
335 OutputFormat::GithubSummary => {
336 print_check_github_format(results, ctx, GithubTarget::Summary)
337 }
338 ci_format => print_results_ci_comment(results, ctx, ci_format),
339 }
340}
341
342#[derive(Clone, Copy)]
344enum GithubTarget {
345 Annotations,
346 Summary,
347}
348
349fn print_github_format(
350 kind: github_annotations::EnvelopeKind,
351 envelope: &serde_json::Value,
352 root: &Path,
353 target: GithubTarget,
354) -> ExitCode {
355 match target {
356 GithubTarget::Annotations => github_annotations::print_annotations(kind, envelope, root),
357 GithubTarget::Summary => github_summary::print_summary(kind, envelope, root),
358 }
359}
360
361fn print_check_github_annotations(results: &AnalysisResults, ctx: &ReportContext<'_>) -> ExitCode {
366 print_check_github_format(results, ctx, GithubTarget::Annotations)
367}
368
369fn print_check_github_format(
370 results: &AnalysisResults,
371 ctx: &ReportContext<'_>,
372 target: GithubTarget,
373) -> ExitCode {
374 match json::api_check_json_document_with_config_fixable_meta_and_extras(
375 results,
376 ctx.root,
377 ctx.elapsed,
378 ctx.config_fixable,
379 None,
380 fallow_api::CheckJsonExtraOutputs::default(),
381 ) {
382 Ok(envelope) => print_github_format(
383 github_annotations::EnvelopeKind::DeadCode,
384 &envelope,
385 ctx.root,
386 target,
387 ),
388 Err(e) => {
389 eprintln!("Error: failed to serialize results: {e}");
390 ExitCode::from(2)
391 }
392 }
393}
394
395fn print_results_ci_comment(
397 results: &AnalysisResults,
398 ctx: &ReportContext<'_>,
399 output: OutputFormat,
400) -> ExitCode {
401 let issues = codeclimate::api_codeclimate_issues(results, ctx.root, ctx.rules);
405 let value = fallow_output::codeclimate_issues_to_value(&issues);
406 print_ci_comment_format("dead-code", &value, output).unwrap_or_else(|| {
407 eprintln!("Error: badge format is only supported for the health command");
408 ExitCode::from(2)
409 })
410}
411
412#[must_use]
414fn print_grouped_results(
415 groups: &[grouping::ResultGroup],
416 original: &AnalysisResults,
417 ctx: &ReportContext<'_>,
418 output: OutputFormat,
419 resolver: &OwnershipResolver,
420) -> ExitCode {
421 match output {
422 OutputFormat::Human => {
423 human::print_grouped_human(&human::PrintGroupedHumanInput {
424 groups,
425 root: ctx.root,
426 rules: ctx.rules,
427 elapsed: ctx.elapsed,
428 quiet: ctx.quiet,
429 resolver: Some(resolver),
430 explain: ctx.explain,
431 });
432 ExitCode::SUCCESS
433 }
434 OutputFormat::Json => json::print_grouped_json(&json::PrintGroupedJsonInput {
435 groups,
436 original,
437 root: ctx.root,
438 elapsed: ctx.elapsed,
439 explain: ctx.explain,
440 type_aware: ctx.type_aware,
441 resolver,
442 config_fixable: ctx.config_fixable,
443 json_style: ctx.json_style,
444 }),
445 OutputFormat::Compact => {
446 compact::print_grouped_compact(groups, ctx.root);
447 compact::print_type_aware_compact(ctx.type_aware, ctx.type_aware_scope);
448 ExitCode::SUCCESS
449 }
450 OutputFormat::Markdown => {
451 markdown::print_grouped_markdown(groups, ctx.root);
452 markdown::print_type_aware_markdown(ctx.type_aware, ctx.type_aware_scope);
453 ExitCode::SUCCESS
454 }
455 OutputFormat::Sarif => {
456 sarif::print_grouped_sarif(original, ctx.root, ctx.rules, resolver, ctx.type_aware)
457 }
458 OutputFormat::CodeClimate => {
459 codeclimate::print_grouped_codeclimate(original, ctx.root, ctx.rules, resolver)
460 }
461 OutputFormat::GithubAnnotations => print_check_github_annotations(original, ctx),
464 OutputFormat::GithubSummary => {
465 print_check_github_format(original, ctx, GithubTarget::Summary)
466 }
467 ci_format => print_results_ci_comment(original, ctx, ci_format),
468 }
469}
470
471#[must_use]
473pub(crate) fn print_duplication_report(
474 report: &DuplicationReport,
475 ctx: &ReportContext<'_>,
476 output: OutputFormat,
477) -> ExitCode {
478 if let Some(ref resolver) = ctx.group_by {
479 let grouping = dupes_grouping::build_duplication_grouping(report, ctx.root, resolver);
480 return print_grouped_duplication_report(report, &grouping, ctx, output, resolver);
481 }
482
483 match output {
484 OutputFormat::Human => {
485 if ctx.summary {
486 human::dupes::print_duplication_summary(
487 report,
488 ctx.elapsed,
489 ctx.quiet,
490 ctx.summary_heading,
491 );
492 } else {
493 human::print_duplication_human(
494 report,
495 ctx.root,
496 ctx.elapsed,
497 ctx.quiet,
498 ctx.show_explain_tip,
499 ctx.explain,
500 );
501 }
502 ExitCode::SUCCESS
503 }
504 OutputFormat::Json => {
505 json::print_duplication_json(report, ctx.root, ctx.elapsed, ctx.explain, ctx.json_style)
506 }
507 OutputFormat::Compact => {
508 compact::print_duplication_compact(report, ctx.root);
509 ExitCode::SUCCESS
510 }
511 OutputFormat::Sarif => sarif::print_duplication_sarif(report, ctx.root),
512 OutputFormat::Markdown => {
513 markdown::print_duplication_markdown(report, ctx.root);
514 ExitCode::SUCCESS
515 }
516 OutputFormat::CodeClimate => codeclimate::print_duplication_codeclimate(report, ctx.root),
517 OutputFormat::GithubAnnotations => {
518 print_dupes_github_format(report, ctx, GithubTarget::Annotations)
519 }
520 OutputFormat::GithubSummary => {
521 print_dupes_github_format(report, ctx, GithubTarget::Summary)
522 }
523 ci_format => print_duplication_ci_comment(report, ctx.root, ci_format),
524 }
525}
526
527fn print_dupes_github_format(
530 report: &DuplicationReport,
531 ctx: &ReportContext<'_>,
532 target: GithubTarget,
533) -> ExitCode {
534 match json::api_duplication_json_document(report, ctx.root, ctx.elapsed, ctx.explain) {
535 Ok(envelope) => print_github_format(
536 github_annotations::EnvelopeKind::Dupes,
537 &envelope,
538 ctx.root,
539 target,
540 ),
541 Err(e) => {
542 eprintln!("Error: failed to serialize duplication report: {e}");
543 ExitCode::from(2)
544 }
545 }
546}
547
548fn print_duplication_ci_comment(
550 report: &DuplicationReport,
551 root: &Path,
552 output: OutputFormat,
553) -> ExitCode {
554 let issues = codeclimate::api_duplication_codeclimate_issues(report, root);
555 let value = fallow_output::codeclimate_issues_to_value(&issues);
556 print_ci_comment_format("dupes", &value, output).unwrap_or_else(|| {
557 eprintln!("Error: badge format is only supported for the health command");
558 ExitCode::from(2)
559 })
560}
561
562#[must_use]
564fn print_grouped_duplication_report(
565 report: &DuplicationReport,
566 grouping: &DuplicationGrouping,
567 ctx: &ReportContext<'_>,
568 output: OutputFormat,
569 resolver: &OwnershipResolver,
570) -> ExitCode {
571 match output {
572 OutputFormat::Human => {
573 human::print_grouped_duplication_human(
574 report,
575 grouping,
576 ctx.root,
577 ctx.elapsed,
578 ctx.quiet,
579 );
580 ExitCode::SUCCESS
581 }
582 OutputFormat::Json => json::print_grouped_duplication_json(
583 report,
584 grouping,
585 ctx.root,
586 ctx.elapsed,
587 ctx.explain,
588 ctx.json_style,
589 ),
590 OutputFormat::Sarif => sarif::print_grouped_duplication_sarif(report, ctx.root, resolver),
591 OutputFormat::CodeClimate => {
592 codeclimate::print_grouped_duplication_codeclimate(report, ctx.root, resolver)
593 }
594 OutputFormat::PrCommentGithub
595 | OutputFormat::PrCommentGitlab
596 | OutputFormat::ReviewGithub
597 | OutputFormat::ReviewGitlab => print_duplication_ci_comment(report, ctx.root, output),
598 OutputFormat::GithubAnnotations => {
601 print_dupes_github_format(report, ctx, GithubTarget::Annotations)
602 }
603 OutputFormat::GithubSummary => {
604 print_dupes_github_format(report, ctx, GithubTarget::Summary)
605 }
606 OutputFormat::Compact => {
607 compact::print_duplication_compact(report, ctx.root);
608 warn_dupes_grouping_unsupported(grouping, "compact");
609 ExitCode::SUCCESS
610 }
611 OutputFormat::Markdown => {
612 markdown::print_duplication_markdown(report, ctx.root);
613 warn_dupes_grouping_unsupported(grouping, "markdown");
614 ExitCode::SUCCESS
615 }
616 OutputFormat::Badge => {
617 eprintln!("Error: badge format is only supported for the health command");
618 ExitCode::from(2)
619 }
620 }
621}
622
623fn print_ci_comment_format(
628 analysis: &str,
629 value: &serde_json::Value,
630 output: OutputFormat,
631) -> Option<ExitCode> {
632 let exit = match output {
633 OutputFormat::PrCommentGithub => {
634 ci::pr_comment::print_pr_comment(analysis, ci::pr_comment::Provider::Github, value)
635 }
636 OutputFormat::PrCommentGitlab => {
637 ci::pr_comment::print_pr_comment(analysis, ci::pr_comment::Provider::Gitlab, value)
638 }
639 OutputFormat::ReviewGithub => {
640 ci::review::print_review_envelope(analysis, ci::pr_comment::Provider::Github, value)
641 }
642 OutputFormat::ReviewGitlab => {
643 ci::review::print_review_envelope(analysis, ci::pr_comment::Provider::Gitlab, value)
644 }
645 _ => return None,
646 };
647 Some(exit)
648}
649
650fn warn_dupes_grouping_unsupported(grouping: &DuplicationGrouping, format: &str) {
651 eprintln!(
652 "note: --group-by {} is not supported for {format} duplication output, falling back to \
653 ungrouped output (use --format json for the full grouped envelope)",
654 grouping.mode
655 );
656}
657
658#[must_use]
675pub(crate) fn print_health_report(
676 report: &fallow_output::HealthReport,
677 grouping: Option<&fallow_output::HealthGrouping>,
678 group_resolver: Option<&grouping::OwnershipResolver>,
679 ctx: &ReportContext<'_>,
680 output: OutputFormat,
681) -> ExitCode {
682 match output {
683 OutputFormat::Human => {
684 print_health_human_report(report, grouping, ctx);
685 ExitCode::SUCCESS
686 }
687 OutputFormat::Compact => {
688 compact::print_health_compact(report, ctx.root);
689 compact::print_type_aware_compact(ctx.type_aware, ctx.type_aware_scope);
690 warn_grouping_unsupported(grouping, "compact");
691 ExitCode::SUCCESS
692 }
693 OutputFormat::Markdown => {
694 markdown::print_health_markdown(report, ctx.root);
695 markdown::print_type_aware_markdown(ctx.type_aware, ctx.type_aware_scope);
696 warn_grouping_unsupported(grouping, "markdown");
697 ExitCode::SUCCESS
698 }
699 OutputFormat::Sarif => match group_resolver {
700 Some(resolver) => {
701 sarif::print_grouped_health_sarif(report, ctx.root, resolver, ctx.type_aware)
702 }
703 None => sarif::print_health_sarif(report, ctx.root, ctx.type_aware),
704 },
705 OutputFormat::Json => match grouping {
706 Some(grouping) => json::print_grouped_health_json(
707 report,
708 grouping,
709 ctx.root,
710 ctx.elapsed,
711 ctx.explain,
712 ctx.type_aware,
713 ctx.json_style,
714 ),
715 None => json::print_health_json(
716 report,
717 ctx.root,
718 ctx.elapsed,
719 ctx.explain,
720 ctx.type_aware,
721 ctx.json_style,
722 ),
723 },
724 OutputFormat::CodeClimate => match group_resolver {
725 Some(resolver) => {
726 codeclimate::print_grouped_health_codeclimate(report, ctx.root, resolver)
727 }
728 None => codeclimate::print_health_codeclimate(report, ctx.root),
729 },
730 OutputFormat::PrCommentGithub
731 | OutputFormat::PrCommentGitlab
732 | OutputFormat::ReviewGithub
733 | OutputFormat::ReviewGitlab => print_health_ci_comment(report, ctx.root, output),
734 OutputFormat::GithubAnnotations => {
737 print_health_github_format(report, ctx, GithubTarget::Annotations)
738 }
739 OutputFormat::GithubSummary => {
740 print_health_github_format(report, ctx, GithubTarget::Summary)
741 }
742 OutputFormat::Badge => {
743 warn_grouping_unsupported(grouping, "badge");
744 badge::print_health_badge(report)
745 }
746 }
747}
748
749fn print_health_github_format(
752 report: &fallow_output::HealthReport,
753 ctx: &ReportContext<'_>,
754 target: GithubTarget,
755) -> ExitCode {
756 match json::api_health_json_document(report, ctx.root, ctx.elapsed, ctx.explain, ctx.type_aware)
757 {
758 Ok(envelope) => print_github_format(
759 github_annotations::EnvelopeKind::Health,
760 &envelope,
761 ctx.root,
762 target,
763 ),
764 Err(e) => {
765 eprintln!("Error: failed to serialize health report: {e}");
766 ExitCode::from(2)
767 }
768 }
769}
770
771fn print_health_human_report(
773 report: &fallow_output::HealthReport,
774 grouping: Option<&fallow_output::HealthGrouping>,
775 ctx: &ReportContext<'_>,
776) {
777 if ctx.summary {
778 human::health::print_health_summary(report, ctx.elapsed, ctx.quiet, ctx.summary_heading);
779 return;
780 }
781 human::print_health_human(&human::PrintHealthHumanInput {
782 report,
783 root: ctx.root,
784 elapsed: ctx.elapsed,
785 quiet: ctx.quiet,
786 show_explain_tip: ctx.show_explain_tip,
787 explain: ctx.explain,
788 skip_score_and_trend: ctx.skip_score_and_trend,
789 css_requested: ctx.css_requested,
790 type_aware: ctx.type_aware,
791 });
792 if let Some(grouping) = grouping {
793 human::print_health_grouping(grouping, ctx.root, ctx.quiet);
794 }
795}
796
797fn print_health_ci_comment(
799 report: &fallow_output::HealthReport,
800 root: &Path,
801 output: OutputFormat,
802) -> ExitCode {
803 let issues = codeclimate::api_health_codeclimate_issues(report, root);
804 let value = fallow_output::codeclimate_issues_to_value(&issues);
805 print_ci_comment_format("health", &value, output).unwrap_or_else(|| {
806 eprintln!("Error: badge format is only supported for the health command");
807 ExitCode::from(2)
808 })
809}
810
811fn warn_grouping_unsupported(grouping: Option<&fallow_output::HealthGrouping>, format: &str) {
812 if let Some(g) = grouping {
813 eprintln!(
814 "note: --group-by {} is not supported for {format} output, falling back to \
815 ungrouped output (use --format json for the full grouped envelope)",
816 g.mode
817 );
818 }
819}
820
821pub(crate) fn print_cross_reference_findings(
825 cross_ref: &fallow_engine::cross_reference::CrossReferenceResult,
826 root: &Path,
827 quiet: bool,
828 output: OutputFormat,
829) {
830 human::print_cross_reference_findings(cross_ref, root, quiet, output);
831}
832
833pub(crate) fn print_export_trace(
835 trace: &ExportTrace,
836 format: OutputFormat,
837 json_style: crate::json_style::JsonStyle,
838) {
839 match format {
840 OutputFormat::Json => json::print_trace_json(trace, json_style),
841 _ => human::print_export_trace_human(trace),
842 }
843}
844
845pub(crate) fn print_semantic_export_trace(
848 trace: &ExportTrace,
849 format: OutputFormat,
850 explain: bool,
851 json_style: crate::json_style::JsonStyle,
852) {
853 match format {
854 OutputFormat::Json => json::print_semantic_trace_json(trace, explain, json_style),
855 _ => human::print_export_trace_human(trace),
856 }
857}
858
859pub(crate) fn print_class_member_trace(
861 trace: &fallow_engine::trace::ClassMemberTrace,
862 format: OutputFormat,
863 json_style: crate::json_style::JsonStyle,
864) {
865 match format {
866 OutputFormat::Json => json::print_trace_json(trace, json_style),
867 _ => human::print_class_member_trace_human(trace),
868 }
869}
870
871pub(crate) fn print_semantic_class_member_trace(
874 trace: &fallow_engine::trace::ClassMemberTrace,
875 format: OutputFormat,
876 explain: bool,
877 json_style: crate::json_style::JsonStyle,
878) {
879 match format {
880 OutputFormat::Json => json::print_semantic_trace_json(trace, explain, json_style),
881 _ => human::print_class_member_trace_human(trace),
882 }
883}
884
885pub(crate) fn print_file_trace(
887 trace: &FileTrace,
888 format: OutputFormat,
889 json_style: crate::json_style::JsonStyle,
890) {
891 match format {
892 OutputFormat::Json => json::print_trace_json(trace, json_style),
893 _ => human::print_file_trace_human(trace),
894 }
895}
896
897pub(crate) fn print_dependency_trace(
899 trace: &DependencyTrace,
900 format: OutputFormat,
901 json_style: crate::json_style::JsonStyle,
902) {
903 match format {
904 OutputFormat::Json => json::print_trace_json(trace, json_style),
905 _ => human::print_dependency_trace_human(trace),
906 }
907}
908
909pub(crate) fn print_clone_trace(
911 trace: &CloneTrace,
912 root: &Path,
913 format: OutputFormat,
914 json_style: crate::json_style::JsonStyle,
915) {
916 match format {
917 OutputFormat::Json => json::print_trace_json(trace, json_style),
918 _ => human::print_clone_trace_human(trace, root),
919 }
920}
921
922pub(crate) fn print_impact_closure_trace(
925 trace: &ImpactClosureTrace,
926 format: OutputFormat,
927 json_style: crate::json_style::JsonStyle,
928) {
929 match format {
930 OutputFormat::Json => json::print_trace_json(trace, json_style),
931 _ => {
932 outln!("Impact closure for {}", trace.seed);
933 outln!(
934 " affected beyond the diff: {} file{}",
935 trace.affected_not_shown.len(),
936 plural(trace.affected_not_shown.len())
937 );
938 for gap in &trace.coordination_gap {
939 outln!(
940 " coordination gap: {} consumes {}",
941 gap.consumer_file,
942 gap.consumed_symbols.join(", ")
943 );
944 }
945 }
946 }
947}
948
949pub(crate) fn print_symbol_impact(
951 impact: &SemanticSymbolImpact,
952 format: OutputFormat,
953 explain: bool,
954 json_style: crate::json_style::JsonStyle,
955) {
956 match format {
957 OutputFormat::Json => json::print_semantic_impact_json(impact, explain, json_style),
958 _ => human::print_symbol_impact_human(impact),
959 }
960}
961
962pub(crate) fn print_performance(
965 timings: &PipelineTimings,
966 format: OutputFormat,
967 json_style: crate::json_style::JsonStyle,
968) {
969 match format {
970 OutputFormat::Json => match json_style.serialize(timings) {
971 Ok(json) => eprintln!("{json}"),
972 Err(e) => eprintln!("Error: failed to serialize timings: {e}"),
973 },
974 _ => human::print_performance_human(timings),
975 }
976}
977
978pub(crate) fn print_health_performance(
981 timings: &fallow_output::HealthTimings,
982 format: OutputFormat,
983 json_style: crate::json_style::JsonStyle,
984) {
985 match format {
986 OutputFormat::Json => match json_style.serialize(timings) {
987 Ok(json) => eprintln!("{json}"),
988 Err(e) => eprintln!("Error: failed to serialize timings: {e}"),
989 },
990 _ => human::print_health_performance_human(timings),
991 }
992}
993
994#[allow(
995 unused_imports,
996 reason = "target-dependent: used in lib, unused in bin"
997)]
998pub use fallow_api::build_compact_lines;
999#[allow(
1000 unused_imports,
1001 reason = "target-dependent: used in lib, unused in bin"
1002)]
1003pub use fallow_api::build_duplication_markdown;
1004#[allow(
1005 unused_imports,
1006 reason = "target-dependent: used in lib, unused in bin"
1007)]
1008pub use fallow_api::build_health_markdown;
1009#[allow(
1010 unused_imports,
1011 reason = "target-dependent: used in lib, unused in bin"
1012)]
1013pub use fallow_api::build_markdown;
1014#[allow(
1015 clippy::redundant_pub_crate,
1016 reason = "pub(crate) deliberately limits visibility, report is pub but these are internal"
1017)]
1018pub(crate) use json::SCHEMA_VERSION;
1019#[allow(
1020 clippy::redundant_pub_crate,
1021 reason = "target-dependent: report is public in lib, private in bin, but this adapter remains crate-internal"
1022)]
1023pub(crate) use json::api_check_json_payload_with_config_fixable;
1024#[allow(
1025 clippy::redundant_pub_crate,
1026 reason = "target-dependent: report is public in lib, private in bin, but these adapters remain crate-internal"
1027)]
1028pub(crate) use json::{build_baseline_deltas_output, check_json_extras};
1029#[allow(
1030 unused_imports,
1031 reason = "target-dependent: used in lib, unused in bin"
1032)]
1033#[allow(
1034 clippy::redundant_pub_crate,
1035 reason = "target-dependent: report is public in lib, private in bin, but this adapter remains crate-internal"
1036)]
1037pub(crate) use sarif::api_health_sarif_document;
1038#[allow(
1039 unused_imports,
1040 reason = "target-dependent: used in lib, unused in bin"
1041)]
1042#[allow(
1043 clippy::redundant_pub_crate,
1044 reason = "target-dependent: report is public in lib, private in bin, but this adapter remains crate-internal"
1045)]
1046pub(crate) use sarif::api_sarif_document;
1047
1048#[cfg(test)]
1049mod tests {
1050 use super::*;
1051 use std::path::{Path, PathBuf};
1052
1053 fn test_context<'a>(root: &'a Path, rules: &'a RulesConfig) -> ReportContext<'a> {
1054 ReportContext {
1055 root,
1056 rules,
1057 elapsed: Duration::default(),
1058 quiet: true,
1059 explain: false,
1060 type_aware: None,
1061 type_aware_scope: None,
1062 group_by: None,
1063 top: None,
1064 summary: false,
1065 summary_heading: false,
1066 show_explain_tip: false,
1067 baseline_matched: None,
1068 config_fixable: false,
1069 skip_score_and_trend: false,
1070 css_requested: false,
1071 json_style: crate::json_style::JsonStyle::Compact,
1072 }
1073 }
1074
1075 #[test]
1076 fn normalize_uri_forward_slashes_unchanged() {
1077 assert_eq!(normalize_uri("src/utils.ts"), "src/utils.ts");
1078 }
1079
1080 #[test]
1081 fn normalize_uri_backslashes_replaced() {
1082 assert_eq!(normalize_uri("src\\utils\\index.ts"), "src/utils/index.ts");
1083 }
1084
1085 #[test]
1086 fn normalize_uri_mixed_slashes() {
1087 assert_eq!(normalize_uri("src\\utils/index.ts"), "src/utils/index.ts");
1088 }
1089
1090 #[test]
1091 fn normalize_uri_path_with_spaces() {
1092 assert_eq!(
1093 normalize_uri("src\\my folder\\file.ts"),
1094 "src/my folder/file.ts"
1095 );
1096 }
1097
1098 #[test]
1099 fn normalize_uri_empty_string() {
1100 assert_eq!(normalize_uri(""), "");
1101 }
1102
1103 #[test]
1104 fn relative_path_strips_root_prefix() {
1105 let root = Path::new("/project");
1106 let path = Path::new("/project/src/utils.ts");
1107 assert_eq!(relative_path(path, root), Path::new("src/utils.ts"));
1108 }
1109
1110 #[test]
1111 fn relative_path_returns_full_path_when_no_prefix() {
1112 let root = Path::new("/other");
1113 let path = Path::new("/project/src/utils.ts");
1114 assert_eq!(relative_path(path, root), path);
1115 }
1116
1117 #[test]
1118 fn relative_path_at_root_returns_empty_or_file() {
1119 let root = Path::new("/project");
1120 let path = Path::new("/project/file.ts");
1121 assert_eq!(relative_path(path, root), Path::new("file.ts"));
1122 }
1123
1124 #[test]
1125 fn relative_path_deeply_nested() {
1126 let root = Path::new("/project");
1127 let path = Path::new("/project/packages/ui/src/components/Button.tsx");
1128 assert_eq!(
1129 relative_path(path, root),
1130 Path::new("packages/ui/src/components/Button.tsx")
1131 );
1132 }
1133
1134 #[test]
1135 fn format_display_path_returns_workspace_relative() {
1136 let root = Path::new("/project");
1137 let path = Path::new("/project/apps/server/src/index.ts");
1138 assert_eq!(format_display_path(path, root), "apps/server/src/index.ts");
1139 }
1140
1141 #[test]
1142 fn format_display_path_collides_in_nx_layout_renders_full_relative() {
1143 let root = Path::new("/project");
1144 let server = Path::new("/project/apps/server/src/index.ts");
1145 let client = Path::new("/project/apps/client/src/index.ts");
1146 assert_eq!(
1147 format_display_path(server, root),
1148 "apps/server/src/index.ts"
1149 );
1150 assert_eq!(
1151 format_display_path(client, root),
1152 "apps/client/src/index.ts"
1153 );
1154 }
1155
1156 #[test]
1157 fn format_display_path_angular_component_renders_parent_directory() {
1158 let root = Path::new("/project");
1159 let path = Path::new(
1160 "/project/apps/admin/src/app/payments/payment-list/payment-list.component.html",
1161 );
1162 assert_eq!(
1163 format_display_path(path, root),
1164 "apps/admin/src/app/payments/payment-list/payment-list.component.html"
1165 );
1166 }
1167
1168 #[test]
1169 fn format_display_path_falls_back_to_full_path_when_root_does_not_prefix() {
1170 let root = Path::new("/other");
1171 let path = Path::new("/project/src/utils.ts");
1172 let rendered = format_display_path(path, root);
1173 assert!(rendered.contains("project"));
1174 assert!(rendered.ends_with("utils.ts"));
1175 assert!(!rendered.contains('\\'));
1176 }
1177
1178 #[test]
1179 fn format_display_path_normalizes_backslashes_to_forward_slashes() {
1180 let root = Path::new("/project");
1181 let path = Path::new("/project/src/sub\\file.ts");
1182 let rendered = format_display_path(path, root);
1183 assert!(
1184 !rendered.contains('\\'),
1185 "backslashes must be normalized: {rendered}"
1186 );
1187 }
1188
1189 #[test]
1190 fn format_display_path_handles_brackets_verbatim() {
1191 let root = Path::new("/project");
1192 let path = Path::new("/project/app/[slug]/page.tsx");
1193 assert_eq!(format_display_path(path, root), "app/[slug]/page.tsx");
1194 }
1195
1196 #[test]
1197 fn format_display_path_path_equals_root_returns_empty() {
1198 let root = Path::new("/project");
1199 let path = Path::new("/project");
1200 assert_eq!(format_display_path(path, root), "");
1201 }
1202
1203 #[test]
1204 fn format_display_path_basename_only_when_path_is_at_root() {
1205 let root = Path::new("/project");
1206 let path = Path::new("/project/Cargo.toml");
1207 assert_eq!(format_display_path(path, root), "Cargo.toml");
1208 }
1209
1210 #[test]
1211 fn relative_uri_produces_forward_slash_path() {
1212 let root = PathBuf::from("/project");
1213 let path = root.join("src").join("utils.ts");
1214 let uri = relative_uri(&path, &root);
1215 assert_eq!(uri, "src/utils.ts");
1216 }
1217
1218 #[test]
1219 fn relative_uri_encodes_brackets() {
1220 let root = PathBuf::from("/project");
1221 let path = root.join("src/app/[...slug]/page.tsx");
1222 let uri = relative_uri(&path, &root);
1223 assert_eq!(uri, "src/app/%5B...slug%5D/page.tsx");
1224 }
1225
1226 #[test]
1227 fn relative_uri_encodes_nested_dynamic_routes() {
1228 let root = PathBuf::from("/project");
1229 let path = root.join("src/app/[slug]/[id]/page.tsx");
1230 let uri = relative_uri(&path, &root);
1231 assert_eq!(uri, "src/app/%5Bslug%5D/%5Bid%5D/page.tsx");
1232 }
1233
1234 #[test]
1235 fn relative_uri_no_common_prefix_returns_full() {
1236 let root = PathBuf::from("/other");
1237 let path = PathBuf::from("/project/src/utils.ts");
1238 let uri = relative_uri(&path, &root);
1239 assert!(uri.contains("project"));
1240 assert!(uri.contains("utils.ts"));
1241 }
1242
1243 #[test]
1244 fn severity_error_maps_to_level_error() {
1245 assert!(matches!(severity_to_level(Severity::Error), Level::Error));
1246 }
1247
1248 #[test]
1249 fn severity_warn_maps_to_level_warn() {
1250 assert!(matches!(severity_to_level(Severity::Warn), Level::Warn));
1251 }
1252
1253 #[test]
1254 fn severity_off_maps_to_level_info() {
1255 assert!(matches!(severity_to_level(Severity::Off), Level::Info));
1256 }
1257
1258 #[test]
1259 fn normalize_uri_single_bracket_pair() {
1260 assert_eq!(normalize_uri("app/[id]/page.tsx"), "app/%5Bid%5D/page.tsx");
1261 }
1262
1263 #[test]
1264 fn normalize_uri_catch_all_route() {
1265 assert_eq!(
1266 normalize_uri("app/[...slug]/page.tsx"),
1267 "app/%5B...slug%5D/page.tsx"
1268 );
1269 }
1270
1271 #[test]
1272 fn normalize_uri_optional_catch_all_route() {
1273 assert_eq!(
1274 normalize_uri("app/[[...slug]]/page.tsx"),
1275 "app/%5B%5B...slug%5D%5D/page.tsx"
1276 );
1277 }
1278
1279 #[test]
1280 fn normalize_uri_multiple_dynamic_segments() {
1281 assert_eq!(
1282 normalize_uri("app/[lang]/posts/[id]"),
1283 "app/%5Blang%5D/posts/%5Bid%5D"
1284 );
1285 }
1286
1287 #[test]
1288 fn normalize_uri_no_special_chars() {
1289 let plain = "src/components/Button.tsx";
1290 assert_eq!(normalize_uri(plain), plain);
1291 }
1292
1293 #[test]
1294 fn normalize_uri_only_backslashes() {
1295 assert_eq!(normalize_uri("a\\b\\c"), "a/b/c");
1296 }
1297
1298 #[test]
1299 fn relative_path_identical_paths_returns_empty() {
1300 let root = Path::new("/project");
1301 assert_eq!(relative_path(root, root), Path::new(""));
1302 }
1303
1304 #[test]
1305 fn relative_path_partial_name_match_not_stripped() {
1306 let root = Path::new("/project");
1307 let path = Path::new("/project-two/src/a.ts");
1308 assert_eq!(relative_path(path, root), path);
1309 }
1310
1311 #[test]
1312 fn relative_uri_combines_stripping_and_encoding() {
1313 let root = PathBuf::from("/project");
1314 let path = root.join("src/app/[slug]/page.tsx");
1315 let uri = relative_uri(&path, &root);
1316 assert_eq!(uri, "src/app/%5Bslug%5D/page.tsx");
1317 assert!(!uri.starts_with('/'));
1318 }
1319
1320 #[test]
1321 fn relative_uri_at_root_file() {
1322 let root = PathBuf::from("/project");
1323 let path = root.join("index.ts");
1324 assert_eq!(relative_uri(&path, &root), "index.ts");
1325 }
1326
1327 #[test]
1328 fn severity_to_level_is_const_evaluable() {
1329 const LEVEL_FROM_ERROR: Level = severity_to_level(Severity::Error);
1330 const LEVEL_FROM_WARN: Level = severity_to_level(Severity::Warn);
1331 const LEVEL_FROM_OFF: Level = severity_to_level(Severity::Off);
1332 assert!(matches!(LEVEL_FROM_ERROR, Level::Error));
1333 assert!(matches!(LEVEL_FROM_WARN, Level::Warn));
1334 assert!(matches!(LEVEL_FROM_OFF, Level::Info));
1335 }
1336
1337 #[test]
1338 fn level_is_copy() {
1339 let level = severity_to_level(Severity::Error);
1340 let copy = level;
1341 assert!(matches!(level, Level::Error));
1342 assert!(matches!(copy, Level::Error));
1343 }
1344
1345 #[test]
1346 fn print_results_rejects_badge_for_dead_code_reports() {
1347 let root = Path::new("/project");
1348 let rules = RulesConfig::default();
1349 let ctx = test_context(root, &rules);
1350
1351 let code = print_results(&AnalysisResults::default(), &ctx, OutputFormat::Badge, None);
1352
1353 assert_eq!(code, ExitCode::from(2));
1354 }
1355
1356 #[test]
1357 fn print_duplication_report_rejects_badge_format() {
1358 let root = Path::new("/project");
1359 let rules = RulesConfig::default();
1360 let ctx = test_context(root, &rules);
1361
1362 let code =
1363 print_duplication_report(&DuplicationReport::default(), &ctx, OutputFormat::Badge);
1364
1365 assert_eq!(code, ExitCode::from(2));
1366 }
1367
1368 #[test]
1369 fn elide_common_prefix_shared_dir() {
1370 assert_eq!(
1371 elide_common_prefix("src/components/A.tsx", "src/components/B.tsx"),
1372 "B.tsx"
1373 );
1374 }
1375
1376 #[test]
1377 fn elide_common_prefix_partial_shared() {
1378 assert_eq!(
1379 elide_common_prefix("src/components/A.tsx", "src/utils/B.tsx"),
1380 "utils/B.tsx"
1381 );
1382 }
1383
1384 #[test]
1385 fn elide_common_prefix_no_shared() {
1386 assert_eq!(
1387 elide_common_prefix("pkg-a/src/A.tsx", "pkg-b/src/B.tsx"),
1388 "pkg-b/src/B.tsx"
1389 );
1390 }
1391
1392 #[test]
1393 fn elide_common_prefix_identical_files() {
1394 assert_eq!(elide_common_prefix("a/b/x.ts", "a/b/y.ts"), "y.ts");
1395 }
1396
1397 #[test]
1398 fn elide_common_prefix_no_dirs() {
1399 assert_eq!(elide_common_prefix("foo.ts", "bar.ts"), "bar.ts");
1400 }
1401
1402 #[test]
1403 fn elide_common_prefix_deep_monorepo() {
1404 assert_eq!(
1405 elide_common_prefix(
1406 "packages/rap/src/rap/components/SearchSelect/SearchSelect.tsx",
1407 "packages/rap/src/rap/components/SearchSelect/SearchSelectItem.tsx"
1408 ),
1409 "SearchSelectItem.tsx"
1410 );
1411 }
1412
1413 #[test]
1414 fn split_dir_filename_with_dir() {
1415 let (dir, file) = split_dir_filename("src/utils/index.ts");
1416 assert_eq!(dir, "src/utils/");
1417 assert_eq!(file, "index.ts");
1418 }
1419
1420 #[test]
1421 fn split_dir_filename_no_dir() {
1422 let (dir, file) = split_dir_filename("file.ts");
1423 assert_eq!(dir, "");
1424 assert_eq!(file, "file.ts");
1425 }
1426
1427 #[test]
1428 fn split_dir_filename_deeply_nested() {
1429 let (dir, file) = split_dir_filename("a/b/c/d/e.ts");
1430 assert_eq!(dir, "a/b/c/d/");
1431 assert_eq!(file, "e.ts");
1432 }
1433
1434 #[test]
1435 fn split_dir_filename_trailing_slash() {
1436 let (dir, file) = split_dir_filename("src/");
1437 assert_eq!(dir, "src/");
1438 assert_eq!(file, "");
1439 }
1440
1441 #[test]
1442 fn split_dir_filename_empty() {
1443 let (dir, file) = split_dir_filename("");
1444 assert_eq!(dir, "");
1445 assert_eq!(file, "");
1446 }
1447
1448 #[test]
1449 fn plural_zero_is_plural() {
1450 assert_eq!(plural(0), "s");
1451 }
1452
1453 #[test]
1454 fn plural_one_is_singular() {
1455 assert_eq!(plural(1), "");
1456 }
1457
1458 #[test]
1459 fn plural_two_is_plural() {
1460 assert_eq!(plural(2), "s");
1461 }
1462
1463 #[test]
1464 fn plural_large_number() {
1465 assert_eq!(plural(999), "s");
1466 }
1467
1468 #[test]
1469 fn elide_common_prefix_empty_base() {
1470 assert_eq!(elide_common_prefix("", "src/foo.ts"), "src/foo.ts");
1471 }
1472
1473 #[test]
1474 fn elide_common_prefix_empty_target() {
1475 assert_eq!(elide_common_prefix("src/foo.ts", ""), "");
1476 }
1477
1478 #[test]
1479 fn elide_common_prefix_both_empty() {
1480 assert_eq!(elide_common_prefix("", ""), "");
1481 }
1482
1483 #[test]
1484 fn elide_common_prefix_same_file_different_extension() {
1485 assert_eq!(
1486 elide_common_prefix("src/utils.ts", "src/utils.js"),
1487 "utils.js"
1488 );
1489 }
1490
1491 #[test]
1492 fn elide_common_prefix_partial_filename_match_not_stripped() {
1493 assert_eq!(
1494 elide_common_prefix("src/App.tsx", "src/AppUtils.tsx"),
1495 "AppUtils.tsx"
1496 );
1497 }
1498
1499 #[test]
1500 fn elide_common_prefix_identical_paths() {
1501 assert_eq!(elide_common_prefix("src/foo.ts", "src/foo.ts"), "foo.ts");
1502 }
1503
1504 #[test]
1505 fn split_dir_filename_single_slash() {
1506 let (dir, file) = split_dir_filename("/file.ts");
1507 assert_eq!(dir, "/");
1508 assert_eq!(file, "file.ts");
1509 }
1510
1511 #[test]
1512 fn emit_json_returns_success_for_valid_value() {
1513 let value = serde_json::json!({"key": "value"});
1514 let code = emit_json(&value, "test");
1515 assert_eq!(code, ExitCode::SUCCESS);
1516 }
1517
1518 mod proptests {
1519 use super::*;
1520 use proptest::prelude::*;
1521
1522 proptest! {
1523 #[test]
1525 fn split_dir_filename_reconstructs_path(path in "[a-zA-Z0-9_./\\-]{0,100}") {
1526 let (dir, file) = split_dir_filename(&path);
1527 let reconstructed = format!("{dir}{file}");
1528 prop_assert_eq!(
1529 reconstructed, path,
1530 "dir+file should reconstruct the original path"
1531 );
1532 }
1533
1534 #[test]
1536 fn plural_returns_empty_or_s(n: usize) {
1537 let result = plural(n);
1538 prop_assert!(
1539 result.is_empty() || result == "s",
1540 "plural should return \"\" or \"s\", got {:?}",
1541 result
1542 );
1543 }
1544
1545 #[test]
1547 fn plural_singular_only_for_one(n: usize) {
1548 let result = plural(n);
1549 if n == 1 {
1550 prop_assert_eq!(result, "", "plural(1) should be empty");
1551 } else {
1552 prop_assert_eq!(result, "s", "plural({}) should be \"s\"", n);
1553 }
1554 }
1555
1556 #[test]
1558 fn normalize_uri_no_backslashes(path in "[a-zA-Z0-9_.\\\\/ \\[\\]%-]{0,100}") {
1559 let result = normalize_uri(&path);
1560 prop_assert!(
1561 !result.contains('\\'),
1562 "Result should not contain backslashes: {result}"
1563 );
1564 }
1565
1566 #[test]
1568 fn normalize_uri_encodes_all_brackets(path in "[a-zA-Z0-9_./\\[\\]%-]{0,80}") {
1569 let result = normalize_uri(&path);
1570 prop_assert!(
1571 !result.contains('[') && !result.contains(']'),
1572 "Result should not contain raw brackets: {result}"
1573 );
1574 }
1575
1576 #[test]
1578 fn elide_common_prefix_returns_suffix_of_target(
1579 base in "[a-zA-Z0-9_./]{0,50}",
1580 target in "[a-zA-Z0-9_./]{0,50}",
1581 ) {
1582 let result = elide_common_prefix(&base, &target);
1583 prop_assert!(
1584 target.ends_with(result),
1585 "Result {:?} should be a suffix of target {:?}",
1586 result, target
1587 );
1588 }
1589
1590 #[test]
1592 fn relative_path_never_panics(
1593 root in "/[a-zA-Z0-9_/]{0,30}",
1594 suffix in "[a-zA-Z0-9_./]{0,30}",
1595 ) {
1596 let root_path = Path::new(&root);
1597 let full = PathBuf::from(format!("{root}/{suffix}"));
1598 let _ = relative_path(&full, root_path);
1599 }
1600 }
1601 }
1602}