1#[path = "dead_code_codeclimate_component_health.rs"]
4mod component_health;
5
6use std::path::Path;
7
8use fallow_config::{RulesConfig, Severity};
9use fallow_output::{
10 CodeClimateIssue, CodeClimateIssueInput, CodeClimateSeverity, build_codeclimate_issue,
11 codeclimate_fingerprint_hash, normalize_uri,
12};
13use fallow_types::identity::dead_code_finding_id;
14use fallow_types::output_dead_code::{
15 EffectiveSeverity, GatedFinding, ReachabilityCaveat, caveat_suffix,
16};
17use fallow_types::results::AnalysisResults;
18
19fn severity_to_codeclimate(s: Severity) -> CodeClimateSeverity {
20 match s {
21 Severity::Error => CodeClimateSeverity::Major,
22 Severity::Warn => CodeClimateSeverity::Minor,
23 Severity::Off => unreachable!(),
24 }
25}
26
27fn gate_codeclimate(effective: Option<EffectiveSeverity>, rule: Severity) -> CodeClimateSeverity {
34 match effective {
35 Some(EffectiveSeverity::Error) => CodeClimateSeverity::Major,
36 Some(EffectiveSeverity::Warn) => CodeClimateSeverity::Minor,
37 None => match rule {
38 Severity::Off => CodeClimateSeverity::Minor,
39 Severity::Error | Severity::Warn => severity_to_codeclimate(rule),
40 },
41 }
42}
43
44fn finding_codeclimate(finding: &impl GatedFinding, rule: Severity) -> CodeClimateSeverity {
45 gate_codeclimate(finding.effective_severity(), rule)
46}
47
48fn cc_path(path: &Path, root: &Path) -> String {
49 normalize_uri(
50 &path
51 .strip_prefix(root)
52 .unwrap_or(path)
53 .display()
54 .to_string(),
55 )
56}
57
58fn cc_caveat_suffix(caveats: &[ReachabilityCaveat]) -> String {
73 caveat_suffix(caveats).unwrap_or_default()
74}
75
76fn dead_code_issue(
95 finding_id: Option<&str>,
96 discriminator: &[&str],
97 input: CodeClimateIssueInput<'_>,
98) -> CodeClimateIssue {
99 let mut issue = build_codeclimate_issue(input);
100 if let Some(id) = finding_id {
101 let mut parts = Vec::with_capacity(discriminator.len() + 1);
102 parts.push(id);
103 parts.extend_from_slice(discriminator);
104 let stable = codeclimate_fingerprint_hash(&parts);
105 if stable != issue.fingerprint {
106 issue.legacy_fingerprint = Some(std::mem::replace(&mut issue.fingerprint, stable));
107 }
108 }
109 issue
110}
111
112fn location_discriminators(locations: &[(String, u32, u32)]) -> Vec<Vec<String>> {
123 locations
124 .iter()
125 .enumerate()
126 .map(|(index, (path, line, col))| {
127 let earlier = locations
128 .iter()
129 .enumerate()
130 .filter(|(other, (other_path, other_line, other_col))| {
131 other_path == path && (other_line, other_col, *other) < (line, col, index)
132 })
133 .count();
134 if earlier == 0 {
135 vec![path.clone()]
136 } else {
137 vec![path.clone(), format!("~{earlier}")]
138 }
139 })
140 .collect()
141}
142
143fn push_dep_cc_issues<'a, I>(
145 issues: &mut Vec<CodeClimateIssue>,
146 deps: I,
147 root: &Path,
148 rule_id: &str,
149 location_label: &str,
150 severity: Severity,
151) where
152 I: IntoIterator<
153 Item = (
154 &'a fallow_types::results::UnusedDependency,
155 &'a [ReachabilityCaveat],
156 Option<EffectiveSeverity>,
157 Option<&'a str>,
158 ),
159 >,
160{
161 for (dep, caveats, effective, finding_id) in deps {
162 let level = gate_codeclimate(effective, severity);
163 let path = cc_path(&dep.path, root);
164 let line = if dep.line > 0 { Some(dep.line) } else { None };
165 let fp = codeclimate_fingerprint_hash(&[rule_id, &dep.package_name]);
166 let workspace_context = if dep.used_in_workspaces.is_empty() {
167 String::new()
168 } else {
169 let workspaces = dep
170 .used_in_workspaces
171 .iter()
172 .map(|path| cc_path(path, root))
173 .collect::<Vec<_>>()
174 .join(", ");
175 format!("; imported in other workspaces: {workspaces}")
176 };
177 issues.push(dead_code_issue(
178 finding_id,
179 &[],
180 CodeClimateIssueInput {
181 check_name: rule_id,
182 description: &format!(
183 "Package '{}' is in {location_label} but never imported{workspace_context}{}",
184 dep.package_name,
185 cc_caveat_suffix(caveats)
186 ),
187 severity: level,
188 category: "Bug Risk",
189 path: &path,
190 begin_line: line,
191 fingerprint: &fp,
192 },
193 ));
194 }
195}
196
197fn push_unused_file_issues(
198 issues: &mut Vec<CodeClimateIssue>,
199 files: &[fallow_types::output_dead_code::UnusedFileFinding],
200 root: &Path,
201 severity: Severity,
202) {
203 if files.is_empty() {
204 return;
205 }
206 for entry in files {
207 let level = finding_codeclimate(entry, severity);
208 let path = cc_path(&entry.file.path, root);
209 let fp = codeclimate_fingerprint_hash(&["fallow/unused-file", &path]);
210 issues.push(dead_code_issue(
211 entry.finding_id.as_deref(),
212 &[],
213 CodeClimateIssueInput {
214 check_name: "fallow/unused-file",
215 description: &format!(
216 "File is not reachable from any entry point{}",
217 cc_caveat_suffix(&entry.reachability_caveats)
218 ),
219 severity: level,
220 category: "Bug Risk",
221 path: &path,
222 begin_line: None,
223 fingerprint: &fp,
224 },
225 ));
226 }
227}
228
229struct UnusedExportIssuesInput<'a, I> {
235 issues: &'a mut Vec<CodeClimateIssue>,
236 exports: I,
237 root: &'a Path,
238 rule_id: &'a str,
239 direct_label: &'a str,
240 re_export_label: &'a str,
241 severity: Severity,
242}
243
244fn push_unused_export_issues<'a, I>(input: UnusedExportIssuesInput<'a, I>)
245where
246 I: IntoIterator<
247 Item = (
248 &'a fallow_types::results::UnusedExport,
249 &'a [ReachabilityCaveat],
250 Option<EffectiveSeverity>,
251 Option<&'a str>,
252 ),
253 >,
254{
255 for (export, caveats, effective, finding_id) in input.exports {
256 let level = gate_codeclimate(effective, input.severity);
257 let path = cc_path(&export.path, input.root);
258 let kind = if export.is_re_export {
259 input.re_export_label
260 } else {
261 input.direct_label
262 };
263 let line_str = export.line.to_string();
264 let fp =
265 codeclimate_fingerprint_hash(&[input.rule_id, &path, &line_str, &export.export_name]);
266 input.issues.push(dead_code_issue(
267 finding_id,
268 &[],
269 CodeClimateIssueInput {
270 check_name: input.rule_id,
271 description: &format!(
272 "{kind} '{}' is never imported by other modules{}",
273 export.export_name,
274 cc_caveat_suffix(caveats)
275 ),
276 severity: level,
277 category: "Bug Risk",
278 path: &path,
279 begin_line: Some(export.line),
280 fingerprint: &fp,
281 },
282 ));
283 }
284}
285
286fn push_private_type_leak_issues(
287 issues: &mut Vec<CodeClimateIssue>,
288 leaks: &[fallow_types::output_dead_code::PrivateTypeLeakFinding],
289 root: &Path,
290 severity: Severity,
291) {
292 if leaks.is_empty() {
293 return;
294 }
295 for entry in leaks {
296 let level = finding_codeclimate(entry, severity);
297 let leak = &entry.leak;
298 let path = cc_path(&leak.path, root);
299 let line_str = leak.line.to_string();
300 let fp = codeclimate_fingerprint_hash(&[
301 "fallow/private-type-leak",
302 &path,
303 &line_str,
304 &leak.export_name,
305 &leak.type_name,
306 ]);
307 issues.push(dead_code_issue(
308 entry.finding_id.as_deref(),
309 &[],
310 CodeClimateIssueInput {
311 check_name: "fallow/private-type-leak",
312 description: &format!(
313 "Export '{}' references private type '{}'",
314 leak.export_name, leak.type_name
315 ),
316 severity: level,
317 category: "Bug Risk",
318 path: &path,
319 begin_line: Some(leak.line),
320 fingerprint: &fp,
321 },
322 ));
323 }
324}
325
326fn push_deprecated_export_issues(
327 issues: &mut Vec<CodeClimateIssue>,
328 findings: &[fallow_types::output_dead_code::DeprecatedExportInUseFinding],
329 root: &Path,
330 severity: Severity,
331) {
332 for entry in findings {
333 let level = finding_codeclimate(entry, severity);
334 let export = &entry.export;
335 let path = cc_path(&export.path, root);
336 let line_str = export.line.to_string();
337 let fp = codeclimate_fingerprint_hash(&[
338 "fallow/deprecated-export-in-use",
339 &path,
340 &line_str,
341 &export.export_name,
342 ]);
343 issues.push(dead_code_issue(
344 entry.finding_id.as_deref(),
345 &[],
346 CodeClimateIssueInput {
347 check_name: "fallow/deprecated-export-in-use",
348 description: &export.description(),
349 severity: level,
350 category: "Compatibility",
351 path: &path,
352 begin_line: Some(export.line),
353 fingerprint: &fp,
354 },
355 ));
356 }
357}
358
359fn push_type_only_dep_issues(
360 issues: &mut Vec<CodeClimateIssue>,
361 deps: &[fallow_types::output_dead_code::TypeOnlyDependencyFinding],
362 root: &Path,
363 severity: Severity,
364) {
365 if deps.is_empty() {
366 return;
367 }
368 for entry in deps {
369 let level = finding_codeclimate(entry, severity);
370 let dep = &entry.dep;
371 let path = cc_path(&dep.path, root);
372 let line = if dep.line > 0 { Some(dep.line) } else { None };
373 let fp = codeclimate_fingerprint_hash(&["fallow/type-only-dependency", &dep.package_name]);
374 issues.push(dead_code_issue(
375 entry.finding_id.as_deref(),
376 &[],
377 CodeClimateIssueInput {
378 check_name: "fallow/type-only-dependency",
379 description: &format!(
380 "Package '{}' is only imported via type-only imports (consider moving to devDependencies)",
381 dep.package_name
382 ),
383 severity: level,
384 category: "Bug Risk",
385 path: &path,
386 begin_line: line,
387 fingerprint: &fp,
388 },
389 ));
390 }
391}
392
393fn push_test_only_dep_issues(
394 issues: &mut Vec<CodeClimateIssue>,
395 deps: &[fallow_types::output_dead_code::TestOnlyDependencyFinding],
396 root: &Path,
397 severity: Severity,
398) {
399 if deps.is_empty() {
400 return;
401 }
402 for entry in deps {
403 let level = finding_codeclimate(entry, severity);
404 let dep = &entry.dep;
405 let path = cc_path(&dep.path, root);
406 let line = if dep.line > 0 { Some(dep.line) } else { None };
407 let fp = codeclimate_fingerprint_hash(&["fallow/test-only-dependency", &dep.package_name]);
408 issues.push(dead_code_issue(
409 entry.finding_id.as_deref(),
410 &[],
411 CodeClimateIssueInput {
412 check_name: "fallow/test-only-dependency",
413 description: &format!(
414 "Package '{}' is only imported by test files (consider moving to devDependencies)",
415 dep.package_name
416 ),
417 severity: level,
418 category: "Bug Risk",
419 path: &path,
420 begin_line: line,
421 fingerprint: &fp,
422 },
423 ));
424 }
425}
426
427fn push_dev_dep_in_prod_issues(
428 issues: &mut Vec<CodeClimateIssue>,
429 deps: &[fallow_types::output_dead_code::DevDependencyInProductionFinding],
430 root: &Path,
431 severity: Severity,
432) {
433 if deps.is_empty() {
434 return;
435 }
436 for entry in deps {
437 let level = finding_codeclimate(entry, severity);
438 let dep = &entry.dep;
439 let path = cc_path(&dep.path, root);
440 let line = if dep.line > 0 { Some(dep.line) } else { None };
441 let fp = codeclimate_fingerprint_hash(&[
442 "fallow/dev-dependency-in-production",
443 &dep.package_name,
444 ]);
445 issues.push(dead_code_issue(
446 entry.finding_id.as_deref(),
447 &[],
448 CodeClimateIssueInput {
449 check_name: "fallow/dev-dependency-in-production",
450 description: &format!(
451 "devDependency '{}' is imported by production code at runtime (consider moving to dependencies)",
452 dep.package_name
453 ),
454 severity: level,
455 category: "Bug Risk",
456 path: &path,
457 begin_line: line,
458 fingerprint: &fp,
459 },
460 ));
461 }
462}
463
464fn push_unused_member_issues<'a, I>(
469 issues: &mut Vec<CodeClimateIssue>,
470 members: I,
471 root: &Path,
472 rule_id: &str,
473 entity_label: &str,
474 severity: Severity,
475) where
476 I: IntoIterator<
477 Item = (
478 &'a fallow_types::results::UnusedMember,
479 &'a [ReachabilityCaveat],
480 Option<EffectiveSeverity>,
481 Option<&'a str>,
482 ),
483 >,
484{
485 for (member, caveats, effective, finding_id) in members {
486 let level = gate_codeclimate(effective, severity);
487 let path = cc_path(&member.path, root);
488 let line_str = member.line.to_string();
489 let fp = codeclimate_fingerprint_hash(&[
490 rule_id,
491 &path,
492 &line_str,
493 &member.parent_name,
494 &member.member_name,
495 ]);
496 issues.push(dead_code_issue(
497 finding_id,
498 &[],
499 CodeClimateIssueInput {
500 check_name: rule_id,
501 description: &format!(
502 "{entity_label} member '{}.{}' is never referenced{}",
503 member.parent_name,
504 member.member_name,
505 cc_caveat_suffix(caveats)
506 ),
507 severity: level,
508 category: "Bug Risk",
509 path: &path,
510 begin_line: Some(member.line),
511 fingerprint: &fp,
512 },
513 ));
514 }
515}
516
517fn push_unresolved_import_issues(
518 issues: &mut Vec<CodeClimateIssue>,
519 imports: &[fallow_types::output_dead_code::UnresolvedImportFinding],
520 root: &Path,
521 severity: Severity,
522) {
523 if imports.is_empty() {
524 return;
525 }
526 for entry in imports {
527 let level = finding_codeclimate(entry, severity);
528 let import = &entry.import;
529 let path = cc_path(&import.path, root);
530 let line_str = import.line.to_string();
531 let fp = codeclimate_fingerprint_hash(&[
532 "fallow/unresolved-import",
533 &path,
534 &line_str,
535 &import.specifier,
536 ]);
537 issues.push(dead_code_issue(
538 entry.finding_id.as_deref(),
539 &[],
540 CodeClimateIssueInput {
541 check_name: "fallow/unresolved-import",
542 description: &format!("Import '{}' could not be resolved", import.specifier),
543 severity: level,
544 category: "Bug Risk",
545 path: &path,
546 begin_line: Some(import.line),
547 fingerprint: &fp,
548 },
549 ));
550 }
551}
552
553fn push_unlisted_dep_issues(
554 issues: &mut Vec<CodeClimateIssue>,
555 deps: &[fallow_types::output_dead_code::UnlistedDependencyFinding],
556 root: &Path,
557 severity: Severity,
558) {
559 if deps.is_empty() {
560 return;
561 }
562 for entry in deps {
563 let level = finding_codeclimate(entry, severity);
564 let dep = &entry.dep;
565 let sites: Vec<(String, u32, u32)> = dep
566 .imported_from
567 .iter()
568 .map(|site| (cc_path(&site.path, root), site.line, site.col))
569 .collect();
570 let discriminators = location_discriminators(&sites);
571 for ((path, line, _), discriminator) in sites.iter().zip(&discriminators) {
572 let line_str = line.to_string();
573 let discriminator: Vec<&str> = discriminator.iter().map(String::as_str).collect();
574 let fp = codeclimate_fingerprint_hash(&[
575 "fallow/unlisted-dependency",
576 path,
577 &line_str,
578 &dep.package_name,
579 ]);
580 issues.push(dead_code_issue(
581 entry.finding_id.as_deref(),
582 &discriminator,
583 CodeClimateIssueInput {
584 check_name: "fallow/unlisted-dependency",
585 description: &format!(
586 "Package '{}' is imported but not listed in package.json",
587 dep.package_name
588 ),
589 severity: level,
590 category: "Bug Risk",
591 path,
592 begin_line: Some(*line),
593 fingerprint: &fp,
594 },
595 ));
596 }
597 }
598}
599
600fn push_duplicate_export_issues(
601 issues: &mut Vec<CodeClimateIssue>,
602 dups: &[fallow_types::output_dead_code::DuplicateExportFinding],
603 root: &Path,
604 severity: Severity,
605) {
606 if dups.is_empty() {
607 return;
608 }
609 for dup in dups {
610 let level = finding_codeclimate(dup, severity);
611 let location_identity = dup
618 .finding_id
619 .is_some()
620 .then(|| dead_code_finding_id("duplicate-export", &[&dup.export.export_name]));
621 let finding_id = location_identity.as_deref();
622 let dup = &dup.export;
623 let locations: Vec<(String, u32, u32)> = dup
624 .locations
625 .iter()
626 .map(|loc| (cc_path(&loc.path, root), loc.line, loc.col))
627 .collect();
628 let discriminators = location_discriminators(&locations);
629 for ((path, line, _), discriminator) in locations.iter().zip(&discriminators) {
630 let line_str = line.to_string();
631 let discriminator: Vec<&str> = discriminator.iter().map(String::as_str).collect();
632 let fp = codeclimate_fingerprint_hash(&[
633 "fallow/duplicate-export",
634 path,
635 &line_str,
636 &dup.export_name,
637 ]);
638 issues.push(dead_code_issue(
639 finding_id,
640 &discriminator,
641 CodeClimateIssueInput {
642 check_name: "fallow/duplicate-export",
643 description: &format!(
644 "Export '{}' appears in multiple modules",
645 dup.export_name
646 ),
647 severity: level,
648 category: "Bug Risk",
649 path,
650 begin_line: Some(*line),
651 fingerprint: &fp,
652 },
653 ));
654 }
655 }
656}
657
658fn push_circular_dep_issues(
659 issues: &mut Vec<CodeClimateIssue>,
660 cycles: &[fallow_types::output_dead_code::CircularDependencyFinding],
661 root: &Path,
662 severity: Severity,
663) {
664 if cycles.is_empty() {
665 return;
666 }
667 for entry in cycles {
668 let level = finding_codeclimate(entry, severity);
669 let cycle = &entry.cycle;
670 let Some(first) = cycle.files.first() else {
671 continue;
672 };
673 let path = cc_path(first, root);
674 let chain: Vec<String> = cycle.files.iter().map(|f| cc_path(f, root)).collect();
675 let chain_str = chain.join(":");
676 let fp = codeclimate_fingerprint_hash(&["fallow/circular-dependency", &chain_str]);
677 let line = if cycle.line > 0 {
678 Some(cycle.line)
679 } else {
680 None
681 };
682 issues.push(dead_code_issue(
683 entry.finding_id.as_deref(),
684 &[],
685 CodeClimateIssueInput {
686 check_name: "fallow/circular-dependency",
687 description: &format!(
688 "Circular dependency{}: {}",
689 if cycle.is_cross_package {
690 " (cross-package)"
691 } else {
692 ""
693 },
694 chain.join(" \u{2192} ")
695 ),
696 severity: level,
697 category: "Bug Risk",
698 path: &path,
699 begin_line: line,
700 fingerprint: &fp,
701 },
702 ));
703 }
704}
705
706fn push_re_export_cycle_issues(
707 issues: &mut Vec<CodeClimateIssue>,
708 cycles: &[fallow_types::output_dead_code::ReExportCycleFinding],
709 root: &Path,
710 severity: Severity,
711) {
712 if cycles.is_empty() {
713 return;
714 }
715 for entry in cycles {
716 let level = finding_codeclimate(entry, severity);
717 let cycle = &entry.cycle;
718 let Some(first) = cycle.files.first() else {
719 continue;
720 };
721 let path = cc_path(first, root);
722 let chain: Vec<String> = cycle.files.iter().map(|f| cc_path(f, root)).collect();
723 let chain_str = chain.join(":");
724 let kind_token = match cycle.kind {
725 fallow_types::results::ReExportCycleKind::SelfLoop => "self-loop",
726 fallow_types::results::ReExportCycleKind::MultiNode => "multi-node",
727 };
728 let kind_tag = match cycle.kind {
729 fallow_types::results::ReExportCycleKind::SelfLoop => " (self-loop)",
730 fallow_types::results::ReExportCycleKind::MultiNode => "",
731 };
732 let fp = codeclimate_fingerprint_hash(&["fallow/re-export-cycle", kind_token, &chain_str]);
733 issues.push(dead_code_issue(
734 entry.finding_id.as_deref(),
735 &[],
736 CodeClimateIssueInput {
737 check_name: "fallow/re-export-cycle",
738 description: &format!("Re-export cycle{}: {}", kind_tag, chain.join(" <-> ")),
739 severity: level,
740 category: "Bug Risk",
741 path: &path,
742 begin_line: None,
743 fingerprint: &fp,
744 },
745 ));
746 }
747}
748
749fn push_package_cycle_issues(
750 issues: &mut Vec<CodeClimateIssue>,
751 cycles: &[fallow_types::output_dead_code::PackageCycleFinding],
752 root: &Path,
753 severity: Severity,
754) {
755 for entry in cycles {
756 let level = finding_codeclimate(entry, severity);
757 let cycle = &entry.cycle;
758 let Some(anchor) = cycle.edges.first() else {
759 continue;
760 };
761 let path = cc_path(&anchor.path, root);
762 let packages = cycle.packages.join(":");
763 let fp = codeclimate_fingerprint_hash(&["fallow/package-cycle", &packages]);
764 let note = if cycle.group_truncated {
765 format!(
766 " ({})",
767 fallow_types::results::PackageCycle::GROUP_TRUNCATED_NOTE
768 )
769 } else {
770 String::new()
771 };
772 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
773 check_name: "fallow/package-cycle",
774 description: &format!("Package cycle: {}{note}", cycle.chain(" \u{2192} ")),
775 severity: level,
776 category: "Bug Risk",
777 path: &path,
778 begin_line: (anchor.line > 0).then_some(anchor.line),
779 fingerprint: &fp,
780 }));
781 }
782}
783
784fn push_boundary_violation_issues(
785 issues: &mut Vec<CodeClimateIssue>,
786 violations: &[fallow_types::output_dead_code::BoundaryViolationFinding],
787 root: &Path,
788 severity: Severity,
789) {
790 if violations.is_empty() {
791 return;
792 }
793 for entry in violations {
794 let level = finding_codeclimate(entry, severity);
795 let v = &entry.violation;
796 let path = cc_path(&v.from_path, root);
797 let to = cc_path(&v.to_path, root);
798 let fp = codeclimate_fingerprint_hash(&["fallow/boundary-violation", &path, &to]);
799 let line = if v.line > 0 { Some(v.line) } else { None };
800 let via = v
801 .via_path
802 .as_ref()
803 .map_or_else(String::new, |via| format!(", via {}", cc_path(via, root)));
804 issues.push(dead_code_issue(
805 entry.finding_id.as_deref(),
806 &[],
807 CodeClimateIssueInput {
808 check_name: "fallow/boundary-violation",
809 description: &format!(
810 "Boundary violation: {} -> {} ({} -> {}{})",
811 path, to, v.from_zone, v.to_zone, via
812 ),
813 severity: level,
814 category: "Bug Risk",
815 path: &path,
816 begin_line: line,
817 fingerprint: &fp,
818 },
819 ));
820 }
821}
822
823fn push_boundary_coverage_issues(
824 issues: &mut Vec<CodeClimateIssue>,
825 violations: &[fallow_types::output_dead_code::BoundaryCoverageViolationFinding],
826 root: &Path,
827 severity: Severity,
828) {
829 if violations.is_empty() {
830 return;
831 }
832 for entry in violations {
833 let level = finding_codeclimate(entry, severity);
834 let v = &entry.violation;
835 let path = cc_path(&v.path, root);
836 let fp = codeclimate_fingerprint_hash(&["fallow/boundary-coverage", &path]);
837 let line = if v.line > 0 { Some(v.line) } else { None };
838 issues.push(dead_code_issue(
839 entry.finding_id.as_deref(),
840 &[],
841 CodeClimateIssueInput {
842 check_name: "fallow/boundary-coverage",
843 description: &format!("Boundary coverage: {path} matches no configured zone"),
844 severity: level,
845 category: "Bug Risk",
846 path: &path,
847 begin_line: line,
848 fingerprint: &fp,
849 },
850 ));
851 }
852}
853
854fn push_boundary_call_issues(
855 issues: &mut Vec<CodeClimateIssue>,
856 violations: &[fallow_types::output_dead_code::BoundaryCallViolationFinding],
857 root: &Path,
858 severity: Severity,
859) {
860 if violations.is_empty() {
861 return;
862 }
863 for entry in violations {
864 let level = finding_codeclimate(entry, severity);
865 let v = &entry.violation;
866 let path = cc_path(&v.path, root);
867 let fp =
868 codeclimate_fingerprint_hash(&["fallow/boundary-call-violation", &path, &v.callee]);
869 let line = if v.line > 0 { Some(v.line) } else { None };
870 issues.push(dead_code_issue(
871 entry.finding_id.as_deref(),
872 &[],
873 CodeClimateIssueInput {
874 check_name: "fallow/boundary-call-violation",
875 description: &format!(
876 "Boundary call: `{}` matches forbidden pattern `{}` in zone '{}'",
877 v.callee, v.pattern, v.zone
878 ),
879 severity: level,
880 category: "Bug Risk",
881 path: &path,
882 begin_line: line,
883 fingerprint: &fp,
884 },
885 ));
886 }
887}
888
889fn push_policy_violation_issues(
890 issues: &mut Vec<CodeClimateIssue>,
891 violations: &[fallow_types::output_dead_code::PolicyViolationFinding],
892 root: &Path,
893) {
894 use fallow_types::results::PolicyViolationSeverity;
895
896 for entry in violations {
897 let v = &entry.violation;
898 let path = cc_path(&v.path, root);
899 let rule = format!("{}/{}", v.pack, v.rule_id);
900 let fp =
901 codeclimate_fingerprint_hash(&["fallow/policy-violation", &path, &rule, &v.matched]);
902 let line = if v.line > 0 { Some(v.line) } else { None };
903 let level = severity_to_codeclimate(match v.severity {
907 PolicyViolationSeverity::Error => Severity::Error,
908 PolicyViolationSeverity::Warn => Severity::Warn,
909 });
910 let message = match &v.message {
911 Some(message) => format!(
912 "Policy violation: `{}` is banned by `{rule}`. {message}",
913 v.matched
914 ),
915 None => format!("Policy violation: `{}` is banned by `{rule}`", v.matched),
916 };
917 issues.push(dead_code_issue(
918 entry.finding_id.as_deref(),
919 &[],
920 CodeClimateIssueInput {
921 check_name: "fallow/policy-violation",
922 description: &message,
923 severity: level,
924 category: "Bug Risk",
925 path: &path,
926 begin_line: line,
927 fingerprint: &fp,
928 },
929 ));
930 }
931}
932
933fn push_invalid_client_export_issues(
934 issues: &mut Vec<CodeClimateIssue>,
935 findings: &[fallow_types::output_dead_code::InvalidClientExportFinding],
936 root: &Path,
937 severity: Severity,
938) {
939 if findings.is_empty() {
940 return;
941 }
942 for entry in findings {
943 let level = finding_codeclimate(entry, severity);
944 let e = &entry.export;
945 let path = cc_path(&e.path, root);
946 let fp =
947 codeclimate_fingerprint_hash(&["fallow/invalid-client-export", &path, &e.export_name]);
948 let line = if e.line > 0 { Some(e.line) } else { None };
949 let message = format!(
950 "Export `{}` is not allowed in a \"{}\" file (Next.js server-only / route-config name)",
951 e.export_name, e.directive
952 );
953 issues.push(dead_code_issue(
954 entry.finding_id.as_deref(),
955 &[],
956 CodeClimateIssueInput {
957 check_name: "fallow/invalid-client-export",
958 description: &message,
959 severity: level,
960 category: "Bug Risk",
961 path: &path,
962 begin_line: line,
963 fingerprint: &fp,
964 },
965 ));
966 }
967}
968
969fn push_mixed_client_server_barrel_issues(
970 issues: &mut Vec<CodeClimateIssue>,
971 findings: &[fallow_types::output_dead_code::MixedClientServerBarrelFinding],
972 root: &Path,
973 severity: Severity,
974) {
975 if findings.is_empty() {
976 return;
977 }
978 for entry in findings {
979 let level = finding_codeclimate(entry, severity);
980 let b = &entry.barrel;
981 let path = cc_path(&b.path, root);
982 let fp = codeclimate_fingerprint_hash(&[
983 "fallow/mixed-client-server-barrel",
984 &path,
985 &b.client_origin,
986 &b.server_origin,
987 ]);
988 let line = if b.line > 0 { Some(b.line) } else { None };
989 let message = format!(
990 "Barrel re-exports both a \"use client\" module (`{}`) and a server-only module (`{}`); one import drags the other's directive across the boundary",
991 b.client_origin, b.server_origin
992 );
993 issues.push(dead_code_issue(
994 entry.finding_id.as_deref(),
995 &[],
996 CodeClimateIssueInput {
997 check_name: "fallow/mixed-client-server-barrel",
998 description: &message,
999 severity: level,
1000 category: "Bug Risk",
1001 path: &path,
1002 begin_line: line,
1003 fingerprint: &fp,
1004 },
1005 ));
1006 }
1007}
1008
1009fn push_misplaced_directive_issues(
1010 issues: &mut Vec<CodeClimateIssue>,
1011 findings: &[fallow_types::output_dead_code::MisplacedDirectiveFinding],
1012 root: &Path,
1013 severity: Severity,
1014) {
1015 if findings.is_empty() {
1016 return;
1017 }
1018 for entry in findings {
1019 let level = finding_codeclimate(entry, severity);
1020 let d = &entry.directive_site;
1021 let path = cc_path(&d.path, root);
1022 let fp = codeclimate_fingerprint_hash(&[
1023 "fallow/misplaced-directive",
1024 &path,
1025 &d.line.to_string(),
1026 &d.directive,
1027 ]);
1028 let line = if d.line > 0 { Some(d.line) } else { None };
1029 let message = format!(
1030 "Directive `\"{}\"` is not in the leading position, so the RSC bundler ignores it; move it to the top of the file",
1031 d.directive
1032 );
1033 issues.push(dead_code_issue(
1034 entry.finding_id.as_deref(),
1035 &[],
1036 CodeClimateIssueInput {
1037 check_name: "fallow/misplaced-directive",
1038 description: &message,
1039 severity: level,
1040 category: "Bug Risk",
1041 path: &path,
1042 begin_line: line,
1043 fingerprint: &fp,
1044 },
1045 ));
1046 }
1047}
1048
1049fn push_unprovided_inject_issues(
1050 issues: &mut Vec<CodeClimateIssue>,
1051 findings: &[fallow_types::output_dead_code::UnprovidedInjectFinding],
1052 root: &Path,
1053 severity: Severity,
1054) {
1055 if findings.is_empty() {
1056 return;
1057 }
1058 for entry in findings {
1059 let level = finding_codeclimate(entry, severity);
1060 let i = &entry.inject;
1061 let path = cc_path(&i.path, root);
1062 let fp = codeclimate_fingerprint_hash(&[
1063 "fallow/unprovided-inject",
1064 &path,
1065 &i.line.to_string(),
1066 &i.key_name,
1067 ]);
1068 let line = if i.line > 0 { Some(i.line) } else { None };
1069 let message = format!(
1070 "inject(`{}`) has no matching provide(`{}`) in this project; at runtime it returns undefined (provide the key or remove this inject)",
1071 i.key_name, i.key_name
1072 );
1073 issues.push(dead_code_issue(
1074 entry.finding_id.as_deref(),
1075 &[],
1076 CodeClimateIssueInput {
1077 check_name: "fallow/unprovided-inject",
1078 description: &message,
1079 severity: level,
1080 category: "Bug Risk",
1081 path: &path,
1082 begin_line: line,
1083 fingerprint: &fp,
1084 },
1085 ));
1086 }
1087}
1088
1089fn push_unrendered_component_issues(
1090 issues: &mut Vec<CodeClimateIssue>,
1091 findings: &[fallow_types::output_dead_code::UnrenderedComponentFinding],
1092 root: &Path,
1093 severity: Severity,
1094) {
1095 if findings.is_empty() {
1096 return;
1097 }
1098 for entry in findings {
1099 let level = finding_codeclimate(entry, severity);
1100 let c = &entry.component;
1101 let path = cc_path(&c.path, root);
1102 let fp = codeclimate_fingerprint_hash(&[
1103 "fallow/unrendered-component",
1104 &path,
1105 &c.line.to_string(),
1106 &c.component_name,
1107 ]);
1108 let line = if c.line > 0 { Some(c.line) } else { None };
1109 let message = format!(
1110 "component `{}` is reachable but rendered nowhere in this project (render it somewhere or remove it)",
1111 c.component_name
1112 );
1113 issues.push(dead_code_issue(
1114 entry.finding_id.as_deref(),
1115 &[],
1116 CodeClimateIssueInput {
1117 check_name: "fallow/unrendered-component",
1118 description: &message,
1119 severity: level,
1120 category: "Bug Risk",
1121 path: &path,
1122 begin_line: line,
1123 fingerprint: &fp,
1124 },
1125 ));
1126 }
1127}
1128
1129fn push_absent_component_prop_issues(
1130 issues: &mut Vec<CodeClimateIssue>,
1131 findings: &[fallow_types::output_dead_code::AbsentComponentPropFinding],
1132 root: &Path,
1133 severity: Severity,
1134) {
1135 for finding in findings {
1136 let prop = &finding.prop;
1137 let path = cc_path(&prop.path, root);
1138 let fingerprint = codeclimate_fingerprint_hash(&[
1139 "fallow/absent-component-prop",
1140 &path,
1141 &prop.component_name,
1142 &prop.prop_name,
1143 ]);
1144 let description = format!(
1145 "Optional prop '{}.{}' is absent from inspected callers. {}",
1146 prop.component_name, prop.prop_name, prop.explanation
1147 );
1148 issues.push(dead_code_issue(
1149 finding.finding_id.as_deref(),
1150 &[],
1151 CodeClimateIssueInput {
1152 check_name: "fallow/absent-component-prop",
1153 description: &description,
1154 severity: finding_codeclimate(finding, severity),
1155 category: "Clarity",
1156 path: &path,
1157 begin_line: Some(prop.line),
1158 fingerprint: &fingerprint,
1159 },
1160 ));
1161 }
1162}
1163
1164fn push_unused_component_prop_issues(
1165 issues: &mut Vec<CodeClimateIssue>,
1166 findings: &[fallow_types::output_dead_code::UnusedComponentPropFinding],
1167 root: &Path,
1168 severity: Severity,
1169) {
1170 if findings.is_empty() {
1171 return;
1172 }
1173 for entry in findings {
1174 let level = finding_codeclimate(entry, severity);
1175 let p = &entry.prop;
1176 let path = cc_path(&p.path, root);
1177 let fp = codeclimate_fingerprint_hash(&[
1178 "fallow/unused-component-prop",
1179 &path,
1180 &p.line.to_string(),
1181 &p.prop_name,
1182 ]);
1183 let line = if p.line > 0 { Some(p.line) } else { None };
1184 let message = format!(
1185 "prop `{}` is declared but referenced nowhere in component `{}` (remove it or use it)",
1186 p.prop_name, p.component_name
1187 );
1188 issues.push(dead_code_issue(
1189 entry.finding_id.as_deref(),
1190 &[],
1191 CodeClimateIssueInput {
1192 check_name: "fallow/unused-component-prop",
1193 description: &message,
1194 severity: level,
1195 category: "Bug Risk",
1196 path: &path,
1197 begin_line: line,
1198 fingerprint: &fp,
1199 },
1200 ));
1201 }
1202}
1203
1204fn push_unused_component_emit_issues(
1205 issues: &mut Vec<CodeClimateIssue>,
1206 findings: &[fallow_types::output_dead_code::UnusedComponentEmitFinding],
1207 root: &Path,
1208 severity: Severity,
1209) {
1210 if findings.is_empty() {
1211 return;
1212 }
1213 for entry in findings {
1214 let level = finding_codeclimate(entry, severity);
1215 let e = &entry.emit;
1216 let path = cc_path(&e.path, root);
1217 let fp = codeclimate_fingerprint_hash(&[
1218 "fallow/unused-component-emit",
1219 &path,
1220 &e.line.to_string(),
1221 &e.emit_name,
1222 ]);
1223 let line = if e.line > 0 { Some(e.line) } else { None };
1224 let message = format!(
1225 "emit `{}` is declared but emitted nowhere in component `{}` (remove it or emit it)",
1226 e.emit_name, e.component_name
1227 );
1228 issues.push(dead_code_issue(
1229 entry.finding_id.as_deref(),
1230 &[],
1231 CodeClimateIssueInput {
1232 check_name: "fallow/unused-component-emit",
1233 description: &message,
1234 severity: level,
1235 category: "Bug Risk",
1236 path: &path,
1237 begin_line: line,
1238 fingerprint: &fp,
1239 },
1240 ));
1241 }
1242}
1243
1244fn push_unused_effect_layer_issues(
1245 issues: &mut Vec<CodeClimateIssue>,
1246 findings: &[fallow_types::output_dead_code::UnusedEffectLayerFinding],
1247 root: &Path,
1248 severity: Severity,
1249) {
1250 for entry in findings {
1251 let level = finding_codeclimate(entry, severity);
1252 let f = &entry.layer;
1253 let path = cc_path(&f.path, root);
1254 let fp = codeclimate_fingerprint_hash(&[
1255 "fallow/unused-effect-layer",
1256 &path,
1257 &f.line.to_string(),
1258 &f.name,
1259 ]);
1260 let line = if f.line > 0 { Some(f.line) } else { None };
1261 let message = format!(
1262 "layer `{}` is composed only into layers that nothing uses ({}); compose it into a live layer or remove it",
1263 f.name,
1264 f.dead_consumers.join(", ")
1265 );
1266 issues.push(dead_code_issue(
1267 entry.finding_id.as_deref(),
1268 &[],
1269 CodeClimateIssueInput {
1270 check_name: "fallow/unused-effect-layer",
1271 description: &message,
1272 severity: level,
1273 category: "Bug Risk",
1274 path: &path,
1275 begin_line: line,
1276 fingerprint: &fp,
1277 },
1278 ));
1279 }
1280}
1281
1282fn push_unused_tagged_error_issues(
1283 issues: &mut Vec<CodeClimateIssue>,
1284 findings: &[fallow_types::output_dead_code::UnusedTaggedErrorFinding],
1285 root: &Path,
1286 severity: Severity,
1287) {
1288 for entry in findings {
1289 let level = finding_codeclimate(entry, severity);
1290 let f = &entry.error;
1291 let path = cc_path(&f.path, root);
1292 let fp = codeclimate_fingerprint_hash(&[
1293 "fallow/unused-tagged-error",
1294 &path,
1295 &f.line.to_string(),
1296 &f.name,
1297 ]);
1298 let line = if f.line > 0 { Some(f.line) } else { None };
1299 let message = format!(
1300 "tagged error `{}` (tag `{}`) is never constructed or failed with; fail with it or remove it",
1301 f.name, f.tag
1302 );
1303 issues.push(dead_code_issue(
1304 entry.finding_id.as_deref(),
1305 &[],
1306 CodeClimateIssueInput {
1307 check_name: "fallow/unused-tagged-error",
1308 description: &message,
1309 severity: level,
1310 category: "Bug Risk",
1311 path: &path,
1312 begin_line: line,
1313 fingerprint: &fp,
1314 },
1315 ));
1316 }
1317}
1318
1319fn push_unproduced_catch_tag_issues(
1320 issues: &mut Vec<CodeClimateIssue>,
1321 findings: &[fallow_types::output_dead_code::UnproducedCatchTagFinding],
1322 root: &Path,
1323 severity: Severity,
1324) {
1325 for entry in findings {
1326 let level = finding_codeclimate(entry, severity);
1327 let f = &entry.catch_tag;
1328 let path = cc_path(&f.path, root);
1329 let fp = codeclimate_fingerprint_hash(&[
1330 "fallow/unproduced-catch-tag",
1331 &path,
1332 &f.line.to_string(),
1333 &f.tag,
1334 ]);
1335 let line = if f.line > 0 { Some(f.line) } else { None };
1336 let message = format!(
1337 "catchTag handler for `{}` never runs: no code produces {}",
1338 f.tag,
1339 f.candidate_classes.join(", ")
1340 );
1341 issues.push(dead_code_issue(
1342 entry.finding_id.as_deref(),
1343 &[],
1344 CodeClimateIssueInput {
1345 check_name: "fallow/unproduced-catch-tag",
1346 description: &message,
1347 severity: level,
1348 category: "Bug Risk",
1349 path: &path,
1350 begin_line: line,
1351 fingerprint: &fp,
1352 },
1353 ));
1354 }
1355}
1356
1357fn push_unused_svelte_event_issues(
1358 issues: &mut Vec<CodeClimateIssue>,
1359 findings: &[fallow_types::output_dead_code::UnusedSvelteEventFinding],
1360 root: &Path,
1361 severity: Severity,
1362) {
1363 if findings.is_empty() {
1364 return;
1365 }
1366 for entry in findings {
1367 let level = finding_codeclimate(entry, severity);
1368 let e = &entry.event;
1369 let path = cc_path(&e.path, root);
1370 let fp = codeclimate_fingerprint_hash(&[
1371 "fallow/unused-svelte-event",
1372 &path,
1373 &e.line.to_string(),
1374 &e.event_name,
1375 ]);
1376 let line = if e.line > 0 { Some(e.line) } else { None };
1377 let message = format!(
1378 "event `{}` is dispatched by component `{}` but listened to nowhere in the project (remove it or listen for it)",
1379 e.event_name, e.component_name
1380 );
1381 issues.push(dead_code_issue(
1382 entry.finding_id.as_deref(),
1383 &[],
1384 CodeClimateIssueInput {
1385 check_name: "fallow/unused-svelte-event",
1386 description: &message,
1387 severity: level,
1388 category: "Bug Risk",
1389 path: &path,
1390 begin_line: line,
1391 fingerprint: &fp,
1392 },
1393 ));
1394 }
1395}
1396
1397fn push_unused_component_input_issues(
1398 issues: &mut Vec<CodeClimateIssue>,
1399 findings: &[fallow_types::output_dead_code::UnusedComponentInputFinding],
1400 root: &Path,
1401 severity: Severity,
1402) {
1403 if findings.is_empty() {
1404 return;
1405 }
1406 for entry in findings {
1407 let level = finding_codeclimate(entry, severity);
1408 let i = &entry.input;
1409 let path = cc_path(&i.path, root);
1410 let fp = codeclimate_fingerprint_hash(&[
1411 "fallow/unused-component-input",
1412 &path,
1413 &i.line.to_string(),
1414 &i.input_name,
1415 ]);
1416 let line = if i.line > 0 { Some(i.line) } else { None };
1417 let message = format!(
1418 "input `{}` is declared but referenced nowhere in component `{}` (remove it or use it)",
1419 i.input_name, i.component_name
1420 );
1421 issues.push(dead_code_issue(
1422 entry.finding_id.as_deref(),
1423 &[],
1424 CodeClimateIssueInput {
1425 check_name: "fallow/unused-component-input",
1426 description: &message,
1427 severity: level,
1428 category: "Bug Risk",
1429 path: &path,
1430 begin_line: line,
1431 fingerprint: &fp,
1432 },
1433 ));
1434 }
1435}
1436
1437fn push_unused_component_output_issues(
1438 issues: &mut Vec<CodeClimateIssue>,
1439 findings: &[fallow_types::output_dead_code::UnusedComponentOutputFinding],
1440 root: &Path,
1441 severity: Severity,
1442) {
1443 if findings.is_empty() {
1444 return;
1445 }
1446 for entry in findings {
1447 let level = finding_codeclimate(entry, severity);
1448 let o = &entry.output;
1449 let path = cc_path(&o.path, root);
1450 let fp = codeclimate_fingerprint_hash(&[
1451 "fallow/unused-component-output",
1452 &path,
1453 &o.line.to_string(),
1454 &o.output_name,
1455 ]);
1456 let line = if o.line > 0 { Some(o.line) } else { None };
1457 let message = format!(
1458 "output `{}` is declared but emitted nowhere in component `{}` (remove it or emit it)",
1459 o.output_name, o.component_name
1460 );
1461 issues.push(dead_code_issue(
1462 entry.finding_id.as_deref(),
1463 &[],
1464 CodeClimateIssueInput {
1465 check_name: "fallow/unused-component-output",
1466 description: &message,
1467 severity: level,
1468 category: "Bug Risk",
1469 path: &path,
1470 begin_line: line,
1471 fingerprint: &fp,
1472 },
1473 ));
1474 }
1475}
1476
1477fn push_unused_server_action_issues(
1478 issues: &mut Vec<CodeClimateIssue>,
1479 findings: &[fallow_types::output_dead_code::UnusedServerActionFinding],
1480 root: &Path,
1481 severity: Severity,
1482) {
1483 if findings.is_empty() {
1484 return;
1485 }
1486 for entry in findings {
1487 let level = finding_codeclimate(entry, severity);
1488 let a = &entry.action;
1489 let path = cc_path(&a.path, root);
1490 let fp = codeclimate_fingerprint_hash(&[
1491 "fallow/unused-server-action",
1492 &path,
1493 &a.line.to_string(),
1494 &a.action_name,
1495 ]);
1496 let line = if a.line > 0 { Some(a.line) } else { None };
1497 let message = format!(
1498 "server action `{}` is exported from a \"use server\" file but no code in this project references it (wire it to a consumer or remove it)",
1499 a.action_name
1500 );
1501 issues.push(dead_code_issue(
1502 entry.finding_id.as_deref(),
1503 &[],
1504 CodeClimateIssueInput {
1505 check_name: "fallow/unused-server-action",
1506 description: &message,
1507 severity: level,
1508 category: "Bug Risk",
1509 path: &path,
1510 begin_line: line,
1511 fingerprint: &fp,
1512 },
1513 ));
1514 }
1515}
1516
1517fn push_unused_load_data_key_issues(
1518 issues: &mut Vec<CodeClimateIssue>,
1519 findings: &[fallow_types::output_dead_code::UnusedLoadDataKeyFinding],
1520 root: &Path,
1521 severity: Severity,
1522) {
1523 if findings.is_empty() {
1524 return;
1525 }
1526 for entry in findings {
1527 let level = finding_codeclimate(entry, severity);
1528 let k = &entry.key;
1529 let path = cc_path(&k.path, root);
1530 let fp = codeclimate_fingerprint_hash(&[
1531 "fallow/unused-load-data-key",
1532 &path,
1533 &k.line.to_string(),
1534 &k.key_name,
1535 ]);
1536 let line = if k.line > 0 { Some(k.line) } else { None };
1537 let message = format!(
1538 "load() return key `{}` is read by no consumer (sibling +page.svelte data.<key> or project-wide page.data.<key>); delete the key or wire a consumer",
1539 k.key_name
1540 );
1541 issues.push(dead_code_issue(
1542 entry.finding_id.as_deref(),
1543 &[],
1544 CodeClimateIssueInput {
1545 check_name: "fallow/unused-load-data-key",
1546 description: &message,
1547 severity: level,
1548 category: "Bug Risk",
1549 path: &path,
1550 begin_line: line,
1551 fingerprint: &fp,
1552 },
1553 ));
1554 }
1555}
1556
1557fn push_route_collision_issues(
1558 issues: &mut Vec<CodeClimateIssue>,
1559 findings: &[fallow_types::output_dead_code::RouteCollisionFinding],
1560 root: &Path,
1561 severity: Severity,
1562) {
1563 if findings.is_empty() {
1564 return;
1565 }
1566 for entry in findings {
1567 let level = finding_codeclimate(entry, severity);
1568 let c = &entry.collision;
1569 let path = cc_path(&c.path, root);
1570 let fp = codeclimate_fingerprint_hash(&["fallow/route-collision", &path, &c.url]);
1571 let line = if c.line > 0 { Some(c.line) } else { None };
1572 let message = format!(
1573 "Route file resolves to `{}`, also owned by {} other file(s); Next.js fails the build because a URL can have only one owner",
1574 c.url,
1575 c.conflicting_paths.len()
1576 );
1577 issues.push(dead_code_issue(
1578 entry.finding_id.as_deref(),
1579 &[],
1580 CodeClimateIssueInput {
1581 check_name: "fallow/route-collision",
1582 description: &message,
1583 severity: level,
1584 category: "Bug Risk",
1585 path: &path,
1586 begin_line: line,
1587 fingerprint: &fp,
1588 },
1589 ));
1590 }
1591}
1592
1593fn push_dynamic_segment_name_conflict_issues(
1594 issues: &mut Vec<CodeClimateIssue>,
1595 findings: &[fallow_types::output_dead_code::DynamicSegmentNameConflictFinding],
1596 root: &Path,
1597 severity: Severity,
1598) {
1599 if findings.is_empty() {
1600 return;
1601 }
1602 for entry in findings {
1603 let level = finding_codeclimate(entry, severity);
1604 let c = &entry.conflict;
1605 let path = cc_path(&c.path, root);
1606 let fp = codeclimate_fingerprint_hash(&[
1607 "fallow/dynamic-segment-name-conflict",
1608 &path,
1609 &c.position,
1610 ]);
1611 let line = if c.line > 0 { Some(c.line) } else { None };
1612 let message = format!(
1613 "Dynamic segments at `{}` use different slug names ({}); Next.js requires one consistent name per dynamic path",
1614 c.position,
1615 c.conflicting_segments.join(", ")
1616 );
1617 issues.push(dead_code_issue(
1618 entry.finding_id.as_deref(),
1619 &[],
1620 CodeClimateIssueInput {
1621 check_name: "fallow/dynamic-segment-name-conflict",
1622 description: &message,
1623 severity: level,
1624 category: "Bug Risk",
1625 path: &path,
1626 begin_line: line,
1627 fingerprint: &fp,
1628 },
1629 ));
1630 }
1631}
1632
1633fn push_stale_suppression_issues(
1634 issues: &mut Vec<CodeClimateIssue>,
1635 suppressions: &[fallow_types::results::StaleSuppression],
1636 root: &Path,
1637 rules: &RulesConfig,
1638) {
1639 if suppressions.is_empty() {
1640 return;
1641 }
1642 for s in suppressions {
1643 let severity = if s.missing_reason {
1644 rules.require_suppression_reason
1645 } else {
1646 rules.stale_suppressions
1647 };
1648 let level = finding_codeclimate(s, severity);
1649 let path = cc_path(&s.path, root);
1650 let line_str = s.line.to_string();
1651 let check_name = if s.missing_reason {
1652 "fallow/missing-suppression-reason"
1653 } else {
1654 "fallow/stale-suppression"
1655 };
1656 let fp = codeclimate_fingerprint_hash(&[check_name, &path, &line_str]);
1657 issues.push(dead_code_issue(
1658 s.finding_id.as_deref(),
1659 &[],
1660 CodeClimateIssueInput {
1661 check_name,
1662 description: &s.display_message(),
1663 severity: level,
1664 category: "Bug Risk",
1665 path: &path,
1666 begin_line: Some(s.line),
1667 fingerprint: &fp,
1668 },
1669 ));
1670 }
1671}
1672
1673fn push_unused_catalog_entry_issues(
1674 issues: &mut Vec<CodeClimateIssue>,
1675 entries: &[fallow_types::output_dead_code::UnusedCatalogEntryFinding],
1676 root: &Path,
1677 severity: Severity,
1678) {
1679 if entries.is_empty() {
1680 return;
1681 }
1682 for entry in entries {
1683 let level = finding_codeclimate(entry, severity);
1684 let finding_id = entry.finding_id.as_deref();
1685 let entry = &entry.entry;
1686 let path = cc_path(&entry.path, root);
1687 let line_str = entry.line.to_string();
1688 let fp = codeclimate_fingerprint_hash(&[
1689 "fallow/unused-catalog-entry",
1690 &path,
1691 &line_str,
1692 &entry.catalog_name,
1693 &entry.entry_name,
1694 ]);
1695 let description = if entry.catalog_name == "default" {
1696 format!(
1697 "Catalog entry '{}' is not referenced by any workspace package",
1698 entry.entry_name
1699 )
1700 } else {
1701 format!(
1702 "Catalog entry '{}' (catalog '{}') is not referenced by any workspace package",
1703 entry.entry_name, entry.catalog_name
1704 )
1705 };
1706 issues.push(dead_code_issue(
1707 finding_id,
1708 &[],
1709 CodeClimateIssueInput {
1710 check_name: "fallow/unused-catalog-entry",
1711 description: &description,
1712 severity: level,
1713 category: "Bug Risk",
1714 path: &path,
1715 begin_line: Some(entry.line),
1716 fingerprint: &fp,
1717 },
1718 ));
1719 }
1720}
1721
1722fn push_unresolved_catalog_reference_issues(
1723 issues: &mut Vec<CodeClimateIssue>,
1724 findings: &[fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding],
1725 root: &Path,
1726 severity: Severity,
1727) {
1728 if findings.is_empty() {
1729 return;
1730 }
1731 for finding in findings {
1732 let level = finding_codeclimate(finding, severity);
1733 let finding_id = finding.finding_id.as_deref();
1734 let finding = &finding.reference;
1735 let path = cc_path(&finding.path, root);
1736 let line_str = finding.line.to_string();
1737 let fp = codeclimate_fingerprint_hash(&[
1738 "fallow/unresolved-catalog-reference",
1739 &path,
1740 &line_str,
1741 &finding.catalog_name,
1742 &finding.entry_name,
1743 ]);
1744 let catalog_phrase = if finding.catalog_name == "default" {
1745 "the default catalog".to_string()
1746 } else {
1747 format!("catalog '{}'", finding.catalog_name)
1748 };
1749 let mut description = format!(
1750 "Package '{}' is referenced via `catalog:{}` but {} does not declare it; `pnpm install` will fail",
1751 finding.entry_name,
1752 if finding.catalog_name == "default" {
1753 ""
1754 } else {
1755 finding.catalog_name.as_str()
1756 },
1757 catalog_phrase,
1758 );
1759 if !finding.available_in_catalogs.is_empty() {
1760 use std::fmt::Write as _;
1761 let _ = write!(
1762 description,
1763 " (available in: {})",
1764 finding.available_in_catalogs.join(", ")
1765 );
1766 }
1767 issues.push(dead_code_issue(
1768 finding_id,
1769 &[],
1770 CodeClimateIssueInput {
1771 check_name: "fallow/unresolved-catalog-reference",
1772 description: &description,
1773 severity: level,
1774 category: "Bug Risk",
1775 path: &path,
1776 begin_line: Some(finding.line),
1777 fingerprint: &fp,
1778 },
1779 ));
1780 }
1781}
1782
1783fn push_empty_catalog_group_issues(
1784 issues: &mut Vec<CodeClimateIssue>,
1785 groups: &[fallow_types::output_dead_code::EmptyCatalogGroupFinding],
1786 root: &Path,
1787 severity: Severity,
1788) {
1789 if groups.is_empty() {
1790 return;
1791 }
1792 for group in groups {
1793 let level = finding_codeclimate(group, severity);
1794 let finding_id = group.finding_id.as_deref();
1795 let group = &group.group;
1796 let path = cc_path(&group.path, root);
1797 let line_str = group.line.to_string();
1798 let fp = codeclimate_fingerprint_hash(&[
1799 "fallow/empty-catalog-group",
1800 &path,
1801 &line_str,
1802 &group.catalog_name,
1803 ]);
1804 issues.push(dead_code_issue(
1805 finding_id,
1806 &[],
1807 CodeClimateIssueInput {
1808 check_name: "fallow/empty-catalog-group",
1809 description: &format!("Catalog group '{}' has no entries", group.catalog_name),
1810 severity: level,
1811 category: "Bug Risk",
1812 path: &path,
1813 begin_line: Some(group.line),
1814 fingerprint: &fp,
1815 },
1816 ));
1817 }
1818}
1819
1820fn push_unused_dependency_override_issues(
1821 issues: &mut Vec<CodeClimateIssue>,
1822 findings: &[fallow_types::output_dead_code::UnusedDependencyOverrideFinding],
1823 root: &Path,
1824 severity: Severity,
1825) {
1826 if findings.is_empty() {
1827 return;
1828 }
1829 for finding in findings {
1830 let level = finding_codeclimate(finding, severity);
1831 let finding_id = finding.finding_id.as_deref();
1832 let finding = &finding.entry;
1833 let path = cc_path(&finding.path, root);
1834 let line_str = finding.line.to_string();
1835 let fp = codeclimate_fingerprint_hash(&[
1836 "fallow/unused-dependency-override",
1837 &path,
1838 &line_str,
1839 finding.source.as_label(),
1840 &finding.raw_key,
1841 ]);
1842 let mut description = format!(
1843 "Override `{}` forces version `{}` but `{}` is not declared by any workspace package or resolved in the lockfile",
1844 finding.raw_key, finding.version_range, finding.target_package,
1845 );
1846 if let Some(hint) = &finding.hint {
1847 use std::fmt::Write as _;
1848 let _ = write!(description, " ({hint})");
1849 }
1850 issues.push(dead_code_issue(
1851 finding_id,
1852 &[],
1853 CodeClimateIssueInput {
1854 check_name: "fallow/unused-dependency-override",
1855 description: &description,
1856 severity: level,
1857 category: "Bug Risk",
1858 path: &path,
1859 begin_line: Some(finding.line),
1860 fingerprint: &fp,
1861 },
1862 ));
1863 }
1864}
1865
1866fn push_misconfigured_dependency_override_issues(
1867 issues: &mut Vec<CodeClimateIssue>,
1868 findings: &[fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding],
1869 root: &Path,
1870 severity: Severity,
1871) {
1872 if findings.is_empty() {
1873 return;
1874 }
1875 for finding in findings {
1876 let level = finding_codeclimate(finding, severity);
1877 let finding_id = finding.finding_id.as_deref();
1878 let finding = &finding.entry;
1879 let path = cc_path(&finding.path, root);
1880 let line_str = finding.line.to_string();
1881 let fp = codeclimate_fingerprint_hash(&[
1882 "fallow/misconfigured-dependency-override",
1883 &path,
1884 &line_str,
1885 finding.source.as_label(),
1886 &finding.raw_key,
1887 ]);
1888 let description = format!(
1889 "Override `{}` -> `{}` is malformed: {}",
1890 finding.raw_key,
1891 finding.raw_value,
1892 finding.reason.describe(),
1893 );
1894 issues.push(dead_code_issue(
1895 finding_id,
1896 &[],
1897 CodeClimateIssueInput {
1898 check_name: "fallow/misconfigured-dependency-override",
1899 description: &description,
1900 severity: level,
1901 category: "Bug Risk",
1902 path: &path,
1903 begin_line: Some(finding.line),
1904 fingerprint: &fp,
1905 },
1906 ));
1907 }
1908}
1909
1910#[must_use]
1917pub fn build_codeclimate(
1918 results: &AnalysisResults,
1919 root: &Path,
1920 rules: &RulesConfig,
1921) -> Vec<CodeClimateIssue> {
1922 CodeClimateBuilder {
1923 issues: Vec::new(),
1924 results,
1925 root,
1926 rules,
1927 }
1928 .build()
1929}
1930
1931struct CodeClimateBuilder<'a> {
1932 issues: Vec<CodeClimateIssue>,
1933 results: &'a AnalysisResults,
1934 root: &'a Path,
1935 rules: &'a RulesConfig,
1936}
1937
1938impl CodeClimateBuilder<'_> {
1939 fn build(mut self) -> Vec<CodeClimateIssue> {
1940 self.push_file_and_export_issues();
1941 self.push_private_type_leak_issues();
1942 push_deprecated_export_issues(
1943 &mut self.issues,
1944 &self.results.deprecated_exports_in_use,
1945 self.root,
1946 self.rules.deprecated_exports_in_use,
1947 );
1948 self.push_package_dependency_issues();
1949 self.push_type_test_dependency_issues();
1950 self.push_member_issues();
1951 self.push_import_and_duplicate_issues();
1952 self.push_graph_issues();
1953 self.push_boundary_issues();
1954 self.push_suppression_and_catalog_issues();
1955 self.push_override_issues();
1956 self.push_prop_drilling_issues();
1957 self.push_component_shape_issues();
1958 self.issues
1959 }
1960
1961 fn push_file_and_export_issues(&mut self) {
1962 push_unused_file_issues(
1963 &mut self.issues,
1964 &self.results.unused_files,
1965 self.root,
1966 self.rules.unused_files,
1967 );
1968 push_unused_export_issues(UnusedExportIssuesInput {
1969 issues: &mut self.issues,
1970 exports: self.results.unused_exports.iter().map(|e| {
1971 (
1972 &e.export,
1973 e.reachability_caveats.as_slice(),
1974 e.effective_severity,
1975 e.finding_id.as_deref(),
1976 )
1977 }),
1978 root: self.root,
1979 rule_id: "fallow/unused-export",
1980 direct_label: "Export",
1981 re_export_label: "Re-export",
1982 severity: self.rules.unused_exports,
1983 });
1984 push_unused_export_issues(UnusedExportIssuesInput {
1985 issues: &mut self.issues,
1986 exports: self.results.unused_types.iter().map(|e| {
1987 (
1988 &e.export,
1989 e.reachability_caveats.as_slice(),
1990 e.effective_severity,
1991 e.finding_id.as_deref(),
1992 )
1993 }),
1994 root: self.root,
1995 rule_id: "fallow/unused-type",
1996 direct_label: "Type export",
1997 re_export_label: "Type re-export",
1998 severity: self.rules.unused_types,
1999 });
2000 }
2001
2002 fn push_private_type_leak_issues(&mut self) {
2003 push_private_type_leak_issues(
2004 &mut self.issues,
2005 &self.results.private_type_leaks,
2006 self.root,
2007 self.rules.private_type_leaks,
2008 );
2009 }
2010
2011 fn push_package_dependency_issues(&mut self) {
2012 push_dep_cc_issues(
2013 &mut self.issues,
2014 self.results.unused_dependencies.iter().map(|f| {
2015 (
2016 &f.dep,
2017 f.reachability_caveats.as_slice(),
2018 f.effective_severity,
2019 f.finding_id.as_deref(),
2020 )
2021 }),
2022 self.root,
2023 "fallow/unused-dependency",
2024 "dependencies",
2025 self.rules.unused_dependencies,
2026 );
2027 push_dep_cc_issues(
2028 &mut self.issues,
2029 self.results.unused_dev_dependencies.iter().map(|f| {
2030 (
2031 &f.dep,
2032 f.reachability_caveats.as_slice(),
2033 f.effective_severity,
2034 f.finding_id.as_deref(),
2035 )
2036 }),
2037 self.root,
2038 "fallow/unused-dev-dependency",
2039 "devDependencies",
2040 self.rules.unused_dev_dependencies,
2041 );
2042 push_dep_cc_issues(
2043 &mut self.issues,
2044 self.results.unused_optional_dependencies.iter().map(|f| {
2045 (
2046 &f.dep,
2047 f.reachability_caveats.as_slice(),
2048 f.effective_severity,
2049 f.finding_id.as_deref(),
2050 )
2051 }),
2052 self.root,
2053 "fallow/unused-optional-dependency",
2054 "optionalDependencies",
2055 self.rules.unused_optional_dependencies,
2056 );
2057 }
2058
2059 fn push_type_test_dependency_issues(&mut self) {
2060 push_type_only_dep_issues(
2061 &mut self.issues,
2062 &self.results.type_only_dependencies,
2063 self.root,
2064 self.rules.type_only_dependencies,
2065 );
2066 push_test_only_dep_issues(
2067 &mut self.issues,
2068 &self.results.test_only_dependencies,
2069 self.root,
2070 self.rules.test_only_dependencies,
2071 );
2072 push_dev_dep_in_prod_issues(
2073 &mut self.issues,
2074 &self.results.dev_dependencies_in_production,
2075 self.root,
2076 self.rules.dev_dependencies_in_production,
2077 );
2078 }
2079
2080 fn push_member_issues(&mut self) {
2081 push_unused_member_issues(
2082 &mut self.issues,
2083 self.results.unused_enum_members.iter().map(|m| {
2084 (
2085 &m.member,
2086 m.reachability_caveats.as_slice(),
2087 m.effective_severity,
2088 m.finding_id.as_deref(),
2089 )
2090 }),
2091 self.root,
2092 "fallow/unused-enum-member",
2093 "Enum",
2094 self.rules.unused_enum_members,
2095 );
2096 push_unused_member_issues(
2097 &mut self.issues,
2098 self.results.unused_class_members.iter().map(|m| {
2099 (
2100 &m.member,
2101 m.reachability_caveats.as_slice(),
2102 m.effective_severity,
2103 m.finding_id.as_deref(),
2104 )
2105 }),
2106 self.root,
2107 "fallow/unused-class-member",
2108 "Class",
2109 self.rules.unused_class_members,
2110 );
2111 push_unused_member_issues(
2112 &mut self.issues,
2113 self.results.unused_store_members.iter().map(|m| {
2114 (
2115 &m.member,
2116 m.reachability_caveats.as_slice(),
2117 m.effective_severity,
2118 m.finding_id.as_deref(),
2119 )
2120 }),
2121 self.root,
2122 "fallow/unused-store-member",
2123 "Store",
2124 self.rules.unused_store_members,
2125 );
2126 }
2127
2128 fn push_import_and_duplicate_issues(&mut self) {
2129 push_unresolved_import_issues(
2130 &mut self.issues,
2131 &self.results.unresolved_imports,
2132 self.root,
2133 self.rules.unresolved_imports,
2134 );
2135 push_unlisted_dep_issues(
2136 &mut self.issues,
2137 &self.results.unlisted_dependencies,
2138 self.root,
2139 self.rules.unlisted_dependencies,
2140 );
2141 push_duplicate_export_issues(
2142 &mut self.issues,
2143 &self.results.duplicate_exports,
2144 self.root,
2145 self.rules.duplicate_exports,
2146 );
2147 }
2148
2149 fn push_graph_issues(&mut self) {
2150 push_circular_dep_issues(
2151 &mut self.issues,
2152 &self.results.circular_dependencies,
2153 self.root,
2154 self.rules.circular_dependencies,
2155 );
2156 push_re_export_cycle_issues(
2157 &mut self.issues,
2158 &self.results.re_export_cycles,
2159 self.root,
2160 self.rules.re_export_cycle,
2161 );
2162 push_package_cycle_issues(
2163 &mut self.issues,
2164 &self.results.package_cycles,
2165 self.root,
2166 self.rules.package_cycle,
2167 );
2168 }
2169
2170 fn push_boundary_issues(&mut self) {
2171 self.push_architecture_boundary_issues();
2172 self.push_client_server_boundary_issues();
2173 self.push_component_boundary_issues();
2174 self.push_framework_route_issues();
2175 }
2176
2177 fn push_architecture_boundary_issues(&mut self) {
2178 push_boundary_violation_issues(
2179 &mut self.issues,
2180 &self.results.boundary_violations,
2181 self.root,
2182 self.rules.boundary_violation,
2183 );
2184 push_boundary_coverage_issues(
2185 &mut self.issues,
2186 &self.results.boundary_coverage_violations,
2187 self.root,
2188 self.rules.boundary_violation,
2189 );
2190 push_boundary_call_issues(
2191 &mut self.issues,
2192 &self.results.boundary_call_violations,
2193 self.root,
2194 self.rules.boundary_violation,
2195 );
2196 push_policy_violation_issues(&mut self.issues, &self.results.policy_violations, self.root);
2197 }
2198
2199 fn push_client_server_boundary_issues(&mut self) {
2200 push_invalid_client_export_issues(
2201 &mut self.issues,
2202 &self.results.invalid_client_exports,
2203 self.root,
2204 self.rules.invalid_client_export,
2205 );
2206 push_mixed_client_server_barrel_issues(
2207 &mut self.issues,
2208 &self.results.mixed_client_server_barrels,
2209 self.root,
2210 self.rules.mixed_client_server_barrel,
2211 );
2212 push_misplaced_directive_issues(
2213 &mut self.issues,
2214 &self.results.misplaced_directives,
2215 self.root,
2216 self.rules.misplaced_directive,
2217 );
2218 }
2219
2220 fn push_component_boundary_issues(&mut self) {
2221 push_unprovided_inject_issues(
2222 &mut self.issues,
2223 &self.results.unprovided_injects,
2224 self.root,
2225 self.rules.unprovided_injects,
2226 );
2227 push_unrendered_component_issues(
2228 &mut self.issues,
2229 &self.results.unrendered_components,
2230 self.root,
2231 self.rules.unrendered_components,
2232 );
2233 push_absent_component_prop_issues(
2234 &mut self.issues,
2235 &self.results.absent_component_props,
2236 self.root,
2237 self.rules.absent_component_props,
2238 );
2239 push_unused_component_prop_issues(
2240 &mut self.issues,
2241 &self.results.unused_component_props,
2242 self.root,
2243 self.rules.unused_component_props,
2244 );
2245 push_unused_component_emit_issues(
2246 &mut self.issues,
2247 &self.results.unused_component_emits,
2248 self.root,
2249 self.rules.unused_component_emits,
2250 );
2251 push_unused_component_input_issues(
2252 &mut self.issues,
2253 &self.results.unused_component_inputs,
2254 self.root,
2255 self.rules.unused_component_inputs,
2256 );
2257 push_unused_component_output_issues(
2258 &mut self.issues,
2259 &self.results.unused_component_outputs,
2260 self.root,
2261 self.rules.unused_component_outputs,
2262 );
2263 push_unused_svelte_event_issues(
2264 &mut self.issues,
2265 &self.results.unused_svelte_events,
2266 self.root,
2267 self.rules.unused_svelte_events,
2268 );
2269 push_unused_effect_layer_issues(
2270 &mut self.issues,
2271 &self.results.unused_effect_layers,
2272 self.root,
2273 self.rules.unused_effect_layers,
2274 );
2275 push_unused_tagged_error_issues(
2276 &mut self.issues,
2277 &self.results.unused_tagged_errors,
2278 self.root,
2279 self.rules.unused_tagged_errors,
2280 );
2281 push_unproduced_catch_tag_issues(
2282 &mut self.issues,
2283 &self.results.unproduced_catch_tags,
2284 self.root,
2285 self.rules.unproduced_catch_tags,
2286 );
2287 }
2288
2289 fn push_framework_route_issues(&mut self) {
2290 push_unused_server_action_issues(
2291 &mut self.issues,
2292 &self.results.unused_server_actions,
2293 self.root,
2294 self.rules.unused_server_actions,
2295 );
2296 push_unused_load_data_key_issues(
2297 &mut self.issues,
2298 &self.results.unused_load_data_keys,
2299 self.root,
2300 self.rules.unused_load_data_keys,
2301 );
2302 push_route_collision_issues(
2303 &mut self.issues,
2304 &self.results.route_collisions,
2305 self.root,
2306 self.rules.route_collision,
2307 );
2308 push_dynamic_segment_name_conflict_issues(
2309 &mut self.issues,
2310 &self.results.dynamic_segment_name_conflicts,
2311 self.root,
2312 self.rules.dynamic_segment_name_conflict,
2313 );
2314 }
2315
2316 fn push_suppression_and_catalog_issues(&mut self) {
2317 push_stale_suppression_issues(
2318 &mut self.issues,
2319 &self.results.stale_suppressions,
2320 self.root,
2321 self.rules,
2322 );
2323 push_unused_catalog_entry_issues(
2324 &mut self.issues,
2325 &self.results.unused_catalog_entries,
2326 self.root,
2327 self.rules.unused_catalog_entries,
2328 );
2329 push_empty_catalog_group_issues(
2330 &mut self.issues,
2331 &self.results.empty_catalog_groups,
2332 self.root,
2333 self.rules.empty_catalog_groups,
2334 );
2335 push_unresolved_catalog_reference_issues(
2336 &mut self.issues,
2337 &self.results.unresolved_catalog_references,
2338 self.root,
2339 self.rules.unresolved_catalog_references,
2340 );
2341 }
2342
2343 fn push_override_issues(&mut self) {
2344 push_unused_dependency_override_issues(
2345 &mut self.issues,
2346 &self.results.unused_dependency_overrides,
2347 self.root,
2348 self.rules.unused_dependency_overrides,
2349 );
2350 push_misconfigured_dependency_override_issues(
2351 &mut self.issues,
2352 &self.results.misconfigured_dependency_overrides,
2353 self.root,
2354 self.rules.misconfigured_dependency_overrides,
2355 );
2356 }
2357}
2358
2359#[cfg(test)]
2360mod tests {
2361 use std::collections::BTreeSet;
2362
2363 use fallow_output::issue_output_contracts;
2364
2365 fn codeclimate_check_name_literals() -> BTreeSet<String> {
2366 let source = [
2367 include_str!("dead_code_codeclimate.rs")
2368 .split("#[cfg(test)]")
2369 .next()
2370 .expect("source before tests"),
2371 include_str!("dead_code_codeclimate_component_health.rs"),
2372 ]
2373 .concat();
2374 let mut literals = BTreeSet::new();
2375 let mut rest = source.as_str();
2376 while let Some(start) = rest.find("\"fallow/") {
2377 let after_quote = &rest[start + 1..];
2378 let Some(end) = after_quote.find('"') else {
2379 break;
2380 };
2381 literals.insert(after_quote[..end].to_owned());
2382 rest = &after_quote[end + 1..];
2383 }
2384 literals
2385 }
2386
2387 #[test]
2388 fn codeclimate_check_names_match_issue_contracts() {
2389 let from_emitter = codeclimate_check_name_literals();
2390 let from_contracts = issue_output_contracts()
2391 .flat_map(|contract| contract.codeclimate_check_names)
2392 .collect::<BTreeSet<_>>();
2393
2394 assert_eq!(from_emitter, from_contracts);
2395 }
2396
2397 mod caveats {
2398 use std::path::{Path, PathBuf};
2399
2400 use fallow_config::RulesConfig;
2401 use fallow_types::extract::MemberKind;
2402 use fallow_types::output_dead_code::{
2403 ReachabilityCaveat, UnusedClassMemberFinding, UnusedDependencyFinding,
2404 UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding,
2405 UnusedStoreMemberFinding,
2406 };
2407 use fallow_types::results::{
2408 AnalysisResults, DependencyLocation, UnusedDependency, UnusedExport, UnusedFile,
2409 UnusedMember,
2410 };
2411
2412 use crate::dead_code_codeclimate::build_codeclimate;
2413
2414 fn results_with(root: &Path, caveated: bool) -> AnalysisResults {
2416 let caveats = if caveated {
2417 vec![ReachabilityCaveat::IncompleteImportGraph]
2418 } else {
2419 Vec::new()
2420 };
2421 let mut results = AnalysisResults::default();
2422
2423 let mut file = UnusedFileFinding::with_actions(UnusedFile {
2424 path: root.join("src/lib.ts"),
2425 });
2426 file.reachability_caveats.clone_from(&caveats);
2427 results.unused_files.push(file);
2428
2429 let mut export = UnusedExportFinding::with_actions(UnusedExport {
2430 path: root.join("src/lib.ts"),
2431 export_name: "needed".to_owned(),
2432 is_type_only: false,
2433 line: 3,
2434 col: 0,
2435 span_start: 0,
2436 is_re_export: false,
2437 deprecated: false,
2438 deprecated_reason: None,
2439 });
2440 export.reachability_caveats.clone_from(&caveats);
2441 results.unused_exports.push(export);
2442
2443 let mut dep = UnusedDependencyFinding::with_actions(UnusedDependency {
2444 package_name: "left-pad".to_owned(),
2445 location: DependencyLocation::Dependencies,
2446 path: root.join("package.json"),
2447 line: 5,
2448 used_in_workspaces: Vec::new(),
2449 declared_and_imported_in: Vec::new(),
2450 });
2451 dep.reachability_caveats.clone_from(&caveats);
2452 results.unused_dependencies.push(dep);
2453
2454 let member = |parent: &str, name: &str, kind| UnusedMember {
2455 path: root.join("src/lib.ts"),
2456 parent_name: parent.to_owned(),
2457 member_name: name.to_owned(),
2458 kind,
2459 line: 7,
2460 col: 2,
2461 };
2462
2463 let mut enum_member = UnusedEnumMemberFinding::with_actions(member(
2464 "Mode",
2465 "Legacy",
2466 MemberKind::EnumMember,
2467 ));
2468 enum_member.reachability_caveats.clone_from(&caveats);
2469 results.unused_enum_members.push(enum_member);
2470
2471 let mut class_member = UnusedClassMemberFinding::with_actions(member(
2472 "Widget",
2473 "render",
2474 MemberKind::ClassMethod,
2475 ));
2476 class_member.reachability_caveats.clone_from(&caveats);
2477 results.unused_class_members.push(class_member);
2478
2479 let mut store_member = UnusedStoreMemberFinding::with_actions(member(
2480 "useCart",
2481 "subtotal",
2482 MemberKind::StoreMember,
2483 ));
2484 store_member.reachability_caveats.clone_from(&caveats);
2485 results.unused_store_members.push(store_member);
2486
2487 results
2488 }
2489
2490 #[test]
2495 fn descriptions_name_the_caveat() {
2496 let root = PathBuf::from("/project");
2497
2498 let issues =
2499 build_codeclimate(&results_with(&root, true), &root, &RulesConfig::default());
2500
2501 let descriptions: Vec<&str> = issues
2502 .iter()
2503 .map(|issue| issue.description.as_str())
2504 .collect();
2505 assert!(
2506 descriptions
2507 .iter()
2508 .all(|description| description.ends_with(" (caveat: incomplete import graph)")),
2509 "every caveated finding hedges its description: {descriptions:?}"
2510 );
2511 assert!(
2512 descriptions.contains(
2513 &"File is not reachable from any entry point (caveat: incomplete import graph)"
2514 ),
2515 "{descriptions:?}"
2516 );
2517 }
2518
2519 #[test]
2523 fn a_clean_run_carries_no_caveat_text() {
2524 let root = PathBuf::from("/project");
2525
2526 let issues =
2527 build_codeclimate(&results_with(&root, false), &root, &RulesConfig::default());
2528
2529 assert!(
2530 issues
2531 .iter()
2532 .all(|issue| !issue.description.contains("caveat")),
2533 "{:?}",
2534 issues
2535 .iter()
2536 .map(|issue| issue.description.as_str())
2537 .collect::<Vec<_>>()
2538 );
2539 }
2540
2541 #[test]
2545 fn the_caveat_does_not_move_the_fingerprint() {
2546 let root = PathBuf::from("/project");
2547
2548 let clean =
2549 build_codeclimate(&results_with(&root, false), &root, &RulesConfig::default());
2550 let caveated =
2551 build_codeclimate(&results_with(&root, true), &root, &RulesConfig::default());
2552
2553 let fingerprints = |issues: &[fallow_output::CodeClimateIssue]| {
2554 issues
2555 .iter()
2556 .map(|issue| issue.fingerprint.clone())
2557 .collect::<Vec<_>>()
2558 };
2559 assert_eq!(fingerprints(&clean), fingerprints(&caveated));
2560 assert_ne!(
2561 clean[0].description, caveated[0].description,
2562 "the guard is only meaningful while the description actually changed"
2563 );
2564 }
2565 }
2566
2567 mod stable_fingerprints {
2568 use std::path::{Path, PathBuf};
2569
2570 use fallow_config::RulesConfig;
2571 use fallow_output::{CodeClimateIssue, codeclimate_fingerprint_hash};
2572 use fallow_types::identity::stamp_dead_code_finding_ids;
2573 use fallow_types::output_dead_code::{
2574 DuplicateExportFinding, UnlistedDependencyFinding, UnusedDependencyFinding,
2575 UnusedExportFinding,
2576 };
2577 use fallow_types::results::{
2578 AnalysisResults, DependencyLocation, DuplicateExport, DuplicateLocation, ImportSite,
2579 UnlistedDependency, UnusedDependency, UnusedExport,
2580 };
2581
2582 use crate::dead_code_codeclimate::build_codeclimate;
2583
2584 fn export(root: &Path, name: &str, line: u32) -> UnusedExportFinding {
2585 UnusedExportFinding::with_actions(UnusedExport {
2586 path: root.join("src/lib.ts"),
2587 export_name: name.to_owned(),
2588 is_type_only: false,
2589 line,
2590 col: 0,
2591 span_start: 0,
2592 is_re_export: false,
2593 deprecated: false,
2594 deprecated_reason: None,
2595 })
2596 }
2597
2598 fn dependency(manifest: PathBuf, line: u32) -> UnusedDependencyFinding {
2599 UnusedDependencyFinding::with_actions(UnusedDependency {
2600 package_name: "left-pad".to_owned(),
2601 location: DependencyLocation::Dependencies,
2602 path: manifest,
2603 line,
2604 used_in_workspaces: Vec::new(),
2605 declared_and_imported_in: Vec::new(),
2606 })
2607 }
2608
2609 fn results(root: &Path, shift: u32) -> AnalysisResults {
2612 let mut results = AnalysisResults::default();
2613 results
2614 .unused_exports
2615 .push(export(root, "first", 3 + shift));
2616 results
2617 .unused_exports
2618 .push(export(root, "second", 9 + shift));
2619 results
2620 .unused_dependencies
2621 .push(dependency(root.join("package.json"), 5 + shift));
2622 results
2623 .unlisted_dependencies
2624 .push(UnlistedDependencyFinding::with_actions(
2625 UnlistedDependency {
2626 package_name: "chalk".to_owned(),
2627 imported_from: vec![
2628 ImportSite {
2629 path: root.join("src/cli.ts"),
2630 line: 2 + shift,
2631 col: 0,
2632 },
2633 ImportSite {
2634 path: root.join("src/cli.ts"),
2635 line: 8 + shift,
2636 col: 0,
2637 },
2638 ],
2639 },
2640 ));
2641 results
2642 .duplicate_exports
2643 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2644 export_name: "Config".to_owned(),
2645 locations: vec![
2646 DuplicateLocation {
2647 path: root.join("src/a.ts"),
2648 line: 4 + shift,
2649 col: 0,
2650 },
2651 DuplicateLocation {
2652 path: root.join("src/b.ts"),
2653 line: 6 + shift,
2654 col: 0,
2655 },
2656 ],
2657 }));
2658 stamp_dead_code_finding_ids(&mut results, root);
2659 results
2660 }
2661
2662 fn fingerprints(issues: &[CodeClimateIssue]) -> Vec<String> {
2663 issues
2664 .iter()
2665 .map(|issue| issue.fingerprint.clone())
2666 .collect()
2667 }
2668
2669 #[test]
2672 fn a_line_shift_keeps_every_dead_code_fingerprint() {
2673 let root = PathBuf::from("/project");
2674 let rules = RulesConfig::default();
2675
2676 let before = build_codeclimate(&results(&root, 0), &root, &rules);
2677 let after = build_codeclimate(&results(&root, 40), &root, &rules);
2678
2679 assert_eq!(before.len(), 7);
2680 assert_eq!(fingerprints(&before), fingerprints(&after));
2681 assert_ne!(
2682 before[0].location.lines.begin, after[0].location.lines.begin,
2683 "the guard is only meaningful while the lines actually moved"
2684 );
2685 let mut unique = fingerprints(&before);
2686 unique.sort();
2687 unique.dedup();
2688 assert_eq!(
2689 unique.len(),
2690 before.len(),
2691 "every issue keeps its own fingerprint"
2692 );
2693 assert_eq!(
2694 before[0].legacy_fingerprint.as_deref(),
2695 Some(
2696 codeclimate_fingerprint_hash(&[
2697 "fallow/unused-export",
2698 "src/lib.ts",
2699 "3",
2700 "first"
2701 ])
2702 .as_str()
2703 ),
2704 "the line-based value stays available to match older review threads"
2705 );
2706 }
2707
2708 fn site(root: &Path, file: &str, line: u32) -> ImportSite {
2709 ImportSite {
2710 path: root.join(file),
2711 line,
2712 col: 0,
2713 }
2714 }
2715
2716 fn location(root: &Path, file: &str, line: u32) -> DuplicateLocation {
2717 DuplicateLocation {
2718 path: root.join(file),
2719 line,
2720 col: 0,
2721 }
2722 }
2723
2724 fn per_location(
2727 root: &Path,
2728 sites: Vec<ImportSite>,
2729 locations: Vec<DuplicateLocation>,
2730 ) -> Vec<(String, u32, String)> {
2731 let mut results = AnalysisResults::default();
2732 results
2733 .unlisted_dependencies
2734 .push(UnlistedDependencyFinding::with_actions(
2735 UnlistedDependency {
2736 package_name: "chalk".to_owned(),
2737 imported_from: sites,
2738 },
2739 ));
2740 results
2741 .duplicate_exports
2742 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2743 export_name: "Config".to_owned(),
2744 locations,
2745 }));
2746 stamp_dead_code_finding_ids(&mut results, root);
2747 build_codeclimate(&results, root, &RulesConfig::default())
2748 .into_iter()
2749 .map(|issue| {
2750 (
2751 format!("{} {}", issue.check_name, issue.location.path),
2752 issue.location.lines.begin,
2753 issue.fingerprint,
2754 )
2755 })
2756 .collect()
2757 }
2758
2759 fn fingerprint_at(issues: &[(String, u32, String)], path: &str, line: u32) -> String {
2760 issues
2761 .iter()
2762 .find(|(p, l, _)| p == path && *l == line)
2763 .map_or_else(
2764 || panic!("no issue at {path}:{line}: {issues:?}"),
2765 |(_, _, fingerprint)| fingerprint.clone(),
2766 )
2767 }
2768
2769 #[test]
2774 fn a_sibling_location_does_not_move_the_other_fingerprints() {
2775 let root = PathBuf::from("/project");
2776 let base = per_location(
2777 &root,
2778 vec![site(&root, "src/b.ts", 4), site(&root, "src/c.ts", 7)],
2779 vec![
2780 location(&root, "src/b.ts", 4),
2781 location(&root, "src/c.ts", 7),
2782 ],
2783 );
2784 let added = per_location(
2785 &root,
2786 vec![
2787 site(&root, "src/a.ts", 1),
2788 site(&root, "src/b.ts", 4),
2789 site(&root, "src/b.ts", 9),
2790 site(&root, "src/c.ts", 7),
2791 ],
2792 vec![
2793 location(&root, "src/a.ts", 1),
2794 location(&root, "src/b.ts", 4),
2795 location(&root, "src/b.ts", 9),
2796 location(&root, "src/c.ts", 7),
2797 ],
2798 );
2799 let removed = per_location(
2800 &root,
2801 vec![site(&root, "src/c.ts", 7)],
2802 vec![
2803 location(&root, "src/c.ts", 7),
2804 location(&root, "src/d.ts", 2),
2805 ],
2806 );
2807
2808 for rule in ["fallow/unlisted-dependency", "fallow/duplicate-export"] {
2809 let pick = |issues: &[(String, u32, String)]| {
2810 issues
2811 .iter()
2812 .filter_map(|(key, line, fingerprint)| {
2813 key.strip_prefix(&format!("{rule} "))
2814 .map(|path| (path.to_owned(), *line, fingerprint.clone()))
2815 })
2816 .collect::<Vec<_>>()
2817 };
2818 let (base, added, removed) = (pick(&base), pick(&added), pick(&removed));
2819 assert_eq!(
2820 fingerprint_at(&base, "src/b.ts", 4),
2821 fingerprint_at(&added, "src/b.ts", 4)
2822 );
2823 assert_eq!(
2824 fingerprint_at(&base, "src/c.ts", 7),
2825 fingerprint_at(&added, "src/c.ts", 7)
2826 );
2827 assert_eq!(
2828 fingerprint_at(&base, "src/c.ts", 7),
2829 fingerprint_at(&removed, "src/c.ts", 7)
2830 );
2831 assert_ne!(
2832 fingerprint_at(&added, "src/b.ts", 4),
2833 fingerprint_at(&added, "src/b.ts", 9),
2834 "two locations with the same content still get two fingerprints"
2835 );
2836 }
2837 }
2838
2839 #[test]
2842 fn the_same_dependency_in_two_workspaces_gets_two_fingerprints() {
2843 let root = PathBuf::from("/project");
2844 let mut results = AnalysisResults::default();
2845 results
2846 .unused_dependencies
2847 .push(dependency(root.join("packages/a/package.json"), 5));
2848 results
2849 .unused_dependencies
2850 .push(dependency(root.join("packages/b/package.json"), 5));
2851 stamp_dead_code_finding_ids(&mut results, &root);
2852
2853 let issues = build_codeclimate(&results, &root, &RulesConfig::default());
2854
2855 assert_eq!(issues.len(), 2);
2856 assert_ne!(issues[0].fingerprint, issues[1].fingerprint);
2857 }
2858
2859 #[test]
2862 fn a_finding_without_an_id_keeps_the_legacy_fingerprint() {
2863 let root = PathBuf::from("/project");
2864 let mut results = AnalysisResults::default();
2865 results.unused_exports.push(export(&root, "first", 3));
2866
2867 let issues = build_codeclimate(&results, &root, &RulesConfig::default());
2868
2869 assert_eq!(
2870 issues[0].fingerprint,
2871 codeclimate_fingerprint_hash(&["fallow/unused-export", "src/lib.ts", "3", "first"])
2872 );
2873 }
2874 }
2875}