1use std::path::Path;
4
5use fallow_config::{RulesConfig, Severity};
6use fallow_output::{
7 CodeClimateIssue, CodeClimateIssueInput, CodeClimateSeverity, build_codeclimate_issue,
8 codeclimate_fingerprint_hash, normalize_uri,
9};
10use fallow_types::output_dead_code::{ReachabilityCaveat, caveat_suffix};
11use fallow_types::results::AnalysisResults;
12
13fn severity_to_codeclimate(s: Severity) -> CodeClimateSeverity {
14 match s {
15 Severity::Error => CodeClimateSeverity::Major,
16 Severity::Warn => CodeClimateSeverity::Minor,
17 Severity::Off => unreachable!(),
18 }
19}
20
21fn cc_path(path: &Path, root: &Path) -> String {
22 normalize_uri(
23 &path
24 .strip_prefix(root)
25 .unwrap_or(path)
26 .display()
27 .to_string(),
28 )
29}
30
31fn fingerprint_hash(parts: &[&str]) -> String {
32 codeclimate_fingerprint_hash(parts)
33}
34
35fn cc_caveat_suffix(caveats: &[ReachabilityCaveat]) -> String {
50 caveat_suffix(caveats).unwrap_or_default()
51}
52
53fn push_dep_cc_issues<'a, I>(
55 issues: &mut Vec<CodeClimateIssue>,
56 deps: I,
57 root: &Path,
58 rule_id: &str,
59 location_label: &str,
60 severity: Severity,
61) where
62 I: IntoIterator<
63 Item = (
64 &'a fallow_types::results::UnusedDependency,
65 &'a [ReachabilityCaveat],
66 ),
67 >,
68{
69 for (dep, caveats) in deps {
70 let level = severity_to_codeclimate(severity);
71 let path = cc_path(&dep.path, root);
72 let line = if dep.line > 0 { Some(dep.line) } else { None };
73 let fp = fingerprint_hash(&[rule_id, &dep.package_name]);
74 let workspace_context = if dep.used_in_workspaces.is_empty() {
75 String::new()
76 } else {
77 let workspaces = dep
78 .used_in_workspaces
79 .iter()
80 .map(|path| cc_path(path, root))
81 .collect::<Vec<_>>()
82 .join(", ");
83 format!("; imported in other workspaces: {workspaces}")
84 };
85 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
86 check_name: rule_id,
87 description: &format!(
88 "Package '{}' is in {location_label} but never imported{workspace_context}{}",
89 dep.package_name,
90 cc_caveat_suffix(caveats)
91 ),
92 severity: level,
93 category: "Bug Risk",
94 path: &path,
95 begin_line: line,
96 fingerprint: &fp,
97 }));
98 }
99}
100
101fn push_unused_file_issues(
102 issues: &mut Vec<CodeClimateIssue>,
103 files: &[fallow_types::output_dead_code::UnusedFileFinding],
104 root: &Path,
105 severity: Severity,
106) {
107 if files.is_empty() {
108 return;
109 }
110 let level = severity_to_codeclimate(severity);
111 for entry in files {
112 let path = cc_path(&entry.file.path, root);
113 let fp = fingerprint_hash(&["fallow/unused-file", &path]);
114 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
115 check_name: "fallow/unused-file",
116 description: &format!(
117 "File is not reachable from any entry point{}",
118 cc_caveat_suffix(&entry.reachability_caveats)
119 ),
120 severity: level,
121 category: "Bug Risk",
122 path: &path,
123 begin_line: None,
124 fingerprint: &fp,
125 }));
126 }
127}
128
129struct UnusedExportIssuesInput<'a, I> {
135 issues: &'a mut Vec<CodeClimateIssue>,
136 exports: I,
137 root: &'a Path,
138 rule_id: &'a str,
139 direct_label: &'a str,
140 re_export_label: &'a str,
141 severity: Severity,
142}
143
144fn push_unused_export_issues<'a, I>(input: UnusedExportIssuesInput<'a, I>)
145where
146 I: IntoIterator<
147 Item = (
148 &'a fallow_types::results::UnusedExport,
149 &'a [ReachabilityCaveat],
150 ),
151 >,
152{
153 for (export, caveats) in input.exports {
154 let level = severity_to_codeclimate(input.severity);
155 let path = cc_path(&export.path, input.root);
156 let kind = if export.is_re_export {
157 input.re_export_label
158 } else {
159 input.direct_label
160 };
161 let line_str = export.line.to_string();
162 let fp = fingerprint_hash(&[input.rule_id, &path, &line_str, &export.export_name]);
163 input
164 .issues
165 .push(build_codeclimate_issue(CodeClimateIssueInput {
166 check_name: input.rule_id,
167 description: &format!(
168 "{kind} '{}' is never imported by other modules{}",
169 export.export_name,
170 cc_caveat_suffix(caveats)
171 ),
172 severity: level,
173 category: "Bug Risk",
174 path: &path,
175 begin_line: Some(export.line),
176 fingerprint: &fp,
177 }));
178 }
179}
180
181fn push_private_type_leak_issues(
182 issues: &mut Vec<CodeClimateIssue>,
183 leaks: &[fallow_types::output_dead_code::PrivateTypeLeakFinding],
184 root: &Path,
185 severity: Severity,
186) {
187 if leaks.is_empty() {
188 return;
189 }
190 let level = severity_to_codeclimate(severity);
191 for entry in leaks {
192 let leak = &entry.leak;
193 let path = cc_path(&leak.path, root);
194 let line_str = leak.line.to_string();
195 let fp = fingerprint_hash(&[
196 "fallow/private-type-leak",
197 &path,
198 &line_str,
199 &leak.export_name,
200 &leak.type_name,
201 ]);
202 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
203 check_name: "fallow/private-type-leak",
204 description: &format!(
205 "Export '{}' references private type '{}'",
206 leak.export_name, leak.type_name
207 ),
208 severity: level,
209 category: "Bug Risk",
210 path: &path,
211 begin_line: Some(leak.line),
212 fingerprint: &fp,
213 }));
214 }
215}
216
217fn push_type_only_dep_issues(
218 issues: &mut Vec<CodeClimateIssue>,
219 deps: &[fallow_types::output_dead_code::TypeOnlyDependencyFinding],
220 root: &Path,
221 severity: Severity,
222) {
223 if deps.is_empty() {
224 return;
225 }
226 let level = severity_to_codeclimate(severity);
227 for entry in deps {
228 let dep = &entry.dep;
229 let path = cc_path(&dep.path, root);
230 let line = if dep.line > 0 { Some(dep.line) } else { None };
231 let fp = fingerprint_hash(&["fallow/type-only-dependency", &dep.package_name]);
232 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
233 check_name: "fallow/type-only-dependency",
234 description: &format!(
235 "Package '{}' is only imported via type-only imports (consider moving to devDependencies)",
236 dep.package_name
237 ),
238 severity: level,
239 category: "Bug Risk",
240 path: &path,
241 begin_line: line,
242 fingerprint: &fp,
243 }));
244 }
245}
246
247fn push_test_only_dep_issues(
248 issues: &mut Vec<CodeClimateIssue>,
249 deps: &[fallow_types::output_dead_code::TestOnlyDependencyFinding],
250 root: &Path,
251 severity: Severity,
252) {
253 if deps.is_empty() {
254 return;
255 }
256 let level = severity_to_codeclimate(severity);
257 for entry in deps {
258 let dep = &entry.dep;
259 let path = cc_path(&dep.path, root);
260 let line = if dep.line > 0 { Some(dep.line) } else { None };
261 let fp = fingerprint_hash(&["fallow/test-only-dependency", &dep.package_name]);
262 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
263 check_name: "fallow/test-only-dependency",
264 description: &format!(
265 "Package '{}' is only imported by test files (consider moving to devDependencies)",
266 dep.package_name
267 ),
268 severity: level,
269 category: "Bug Risk",
270 path: &path,
271 begin_line: line,
272 fingerprint: &fp,
273 }));
274 }
275}
276
277fn push_dev_dep_in_prod_issues(
278 issues: &mut Vec<CodeClimateIssue>,
279 deps: &[fallow_types::output_dead_code::DevDependencyInProductionFinding],
280 root: &Path,
281 severity: Severity,
282) {
283 if deps.is_empty() {
284 return;
285 }
286 let level = severity_to_codeclimate(severity);
287 for entry in deps {
288 let dep = &entry.dep;
289 let path = cc_path(&dep.path, root);
290 let line = if dep.line > 0 { Some(dep.line) } else { None };
291 let fp = fingerprint_hash(&["fallow/dev-dependency-in-production", &dep.package_name]);
292 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
293 check_name: "fallow/dev-dependency-in-production",
294 description: &format!(
295 "devDependency '{}' is imported by production code at runtime (consider moving to dependencies)",
296 dep.package_name
297 ),
298 severity: level,
299 category: "Bug Risk",
300 path: &path,
301 begin_line: line,
302 fingerprint: &fp,
303 }));
304 }
305}
306
307fn push_unused_member_issues<'a, I>(
312 issues: &mut Vec<CodeClimateIssue>,
313 members: I,
314 root: &Path,
315 rule_id: &str,
316 entity_label: &str,
317 severity: Severity,
318) where
319 I: IntoIterator<
320 Item = (
321 &'a fallow_types::results::UnusedMember,
322 &'a [ReachabilityCaveat],
323 ),
324 >,
325{
326 for (member, caveats) in members {
327 let level = severity_to_codeclimate(severity);
328 let path = cc_path(&member.path, root);
329 let line_str = member.line.to_string();
330 let fp = fingerprint_hash(&[
331 rule_id,
332 &path,
333 &line_str,
334 &member.parent_name,
335 &member.member_name,
336 ]);
337 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
338 check_name: rule_id,
339 description: &format!(
340 "{entity_label} member '{}.{}' is never referenced{}",
341 member.parent_name,
342 member.member_name,
343 cc_caveat_suffix(caveats)
344 ),
345 severity: level,
346 category: "Bug Risk",
347 path: &path,
348 begin_line: Some(member.line),
349 fingerprint: &fp,
350 }));
351 }
352}
353
354fn push_unresolved_import_issues(
355 issues: &mut Vec<CodeClimateIssue>,
356 imports: &[fallow_types::output_dead_code::UnresolvedImportFinding],
357 root: &Path,
358 severity: Severity,
359) {
360 if imports.is_empty() {
361 return;
362 }
363 let level = severity_to_codeclimate(severity);
364 for entry in imports {
365 let import = &entry.import;
366 let path = cc_path(&import.path, root);
367 let line_str = import.line.to_string();
368 let fp = fingerprint_hash(&[
369 "fallow/unresolved-import",
370 &path,
371 &line_str,
372 &import.specifier,
373 ]);
374 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
375 check_name: "fallow/unresolved-import",
376 description: &format!("Import '{}' could not be resolved", import.specifier),
377 severity: level,
378 category: "Bug Risk",
379 path: &path,
380 begin_line: Some(import.line),
381 fingerprint: &fp,
382 }));
383 }
384}
385
386fn push_unlisted_dep_issues(
387 issues: &mut Vec<CodeClimateIssue>,
388 deps: &[fallow_types::output_dead_code::UnlistedDependencyFinding],
389 root: &Path,
390 severity: Severity,
391) {
392 if deps.is_empty() {
393 return;
394 }
395 let level = severity_to_codeclimate(severity);
396 for entry in deps {
397 let dep = &entry.dep;
398 for site in &dep.imported_from {
399 let path = cc_path(&site.path, root);
400 let line_str = site.line.to_string();
401 let fp = fingerprint_hash(&[
402 "fallow/unlisted-dependency",
403 &path,
404 &line_str,
405 &dep.package_name,
406 ]);
407 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
408 check_name: "fallow/unlisted-dependency",
409 description: &format!(
410 "Package '{}' is imported but not listed in package.json",
411 dep.package_name
412 ),
413 severity: level,
414 category: "Bug Risk",
415 path: &path,
416 begin_line: Some(site.line),
417 fingerprint: &fp,
418 }));
419 }
420 }
421}
422
423fn push_duplicate_export_issues(
424 issues: &mut Vec<CodeClimateIssue>,
425 dups: &[fallow_types::output_dead_code::DuplicateExportFinding],
426 root: &Path,
427 severity: Severity,
428) {
429 if dups.is_empty() {
430 return;
431 }
432 let level = severity_to_codeclimate(severity);
433 for dup in dups {
434 let dup = &dup.export;
435 for loc in &dup.locations {
436 let path = cc_path(&loc.path, root);
437 let line_str = loc.line.to_string();
438 let fp = fingerprint_hash(&[
439 "fallow/duplicate-export",
440 &path,
441 &line_str,
442 &dup.export_name,
443 ]);
444 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
445 check_name: "fallow/duplicate-export",
446 description: &format!("Export '{}' appears in multiple modules", dup.export_name),
447 severity: level,
448 category: "Bug Risk",
449 path: &path,
450 begin_line: Some(loc.line),
451 fingerprint: &fp,
452 }));
453 }
454 }
455}
456
457fn push_circular_dep_issues(
458 issues: &mut Vec<CodeClimateIssue>,
459 cycles: &[fallow_types::output_dead_code::CircularDependencyFinding],
460 root: &Path,
461 severity: Severity,
462) {
463 if cycles.is_empty() {
464 return;
465 }
466 let level = severity_to_codeclimate(severity);
467 for entry in cycles {
468 let cycle = &entry.cycle;
469 let Some(first) = cycle.files.first() else {
470 continue;
471 };
472 let path = cc_path(first, root);
473 let chain: Vec<String> = cycle.files.iter().map(|f| cc_path(f, root)).collect();
474 let chain_str = chain.join(":");
475 let fp = fingerprint_hash(&["fallow/circular-dependency", &chain_str]);
476 let line = if cycle.line > 0 {
477 Some(cycle.line)
478 } else {
479 None
480 };
481 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
482 check_name: "fallow/circular-dependency",
483 description: &format!(
484 "Circular dependency{}: {}",
485 if cycle.is_cross_package {
486 " (cross-package)"
487 } else {
488 ""
489 },
490 chain.join(" \u{2192} ")
491 ),
492 severity: level,
493 category: "Bug Risk",
494 path: &path,
495 begin_line: line,
496 fingerprint: &fp,
497 }));
498 }
499}
500
501fn push_re_export_cycle_issues(
502 issues: &mut Vec<CodeClimateIssue>,
503 cycles: &[fallow_types::output_dead_code::ReExportCycleFinding],
504 root: &Path,
505 severity: Severity,
506) {
507 if cycles.is_empty() {
508 return;
509 }
510 let level = severity_to_codeclimate(severity);
511 for entry in cycles {
512 let cycle = &entry.cycle;
513 let Some(first) = cycle.files.first() else {
514 continue;
515 };
516 let path = cc_path(first, root);
517 let chain: Vec<String> = cycle.files.iter().map(|f| cc_path(f, root)).collect();
518 let chain_str = chain.join(":");
519 let kind_token = match cycle.kind {
520 fallow_types::results::ReExportCycleKind::SelfLoop => "self-loop",
521 fallow_types::results::ReExportCycleKind::MultiNode => "multi-node",
522 };
523 let kind_tag = match cycle.kind {
524 fallow_types::results::ReExportCycleKind::SelfLoop => " (self-loop)",
525 fallow_types::results::ReExportCycleKind::MultiNode => "",
526 };
527 let fp = fingerprint_hash(&["fallow/re-export-cycle", kind_token, &chain_str]);
528 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
529 check_name: "fallow/re-export-cycle",
530 description: &format!("Re-export cycle{}: {}", kind_tag, chain.join(" <-> ")),
531 severity: level,
532 category: "Bug Risk",
533 path: &path,
534 begin_line: None,
535 fingerprint: &fp,
536 }));
537 }
538}
539
540fn push_boundary_violation_issues(
541 issues: &mut Vec<CodeClimateIssue>,
542 violations: &[fallow_types::output_dead_code::BoundaryViolationFinding],
543 root: &Path,
544 severity: Severity,
545) {
546 if violations.is_empty() {
547 return;
548 }
549 let level = severity_to_codeclimate(severity);
550 for entry in violations {
551 let v = &entry.violation;
552 let path = cc_path(&v.from_path, root);
553 let to = cc_path(&v.to_path, root);
554 let fp = fingerprint_hash(&["fallow/boundary-violation", &path, &to]);
555 let line = if v.line > 0 { Some(v.line) } else { None };
556 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
557 check_name: "fallow/boundary-violation",
558 description: &format!(
559 "Boundary violation: {} -> {} ({} -> {})",
560 path, to, v.from_zone, v.to_zone
561 ),
562 severity: level,
563 category: "Bug Risk",
564 path: &path,
565 begin_line: line,
566 fingerprint: &fp,
567 }));
568 }
569}
570
571fn push_boundary_coverage_issues(
572 issues: &mut Vec<CodeClimateIssue>,
573 violations: &[fallow_types::output_dead_code::BoundaryCoverageViolationFinding],
574 root: &Path,
575 severity: Severity,
576) {
577 if violations.is_empty() {
578 return;
579 }
580 let level = severity_to_codeclimate(severity);
581 for entry in violations {
582 let v = &entry.violation;
583 let path = cc_path(&v.path, root);
584 let fp = fingerprint_hash(&["fallow/boundary-coverage", &path]);
585 let line = if v.line > 0 { Some(v.line) } else { None };
586 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
587 check_name: "fallow/boundary-coverage",
588 description: &format!("Boundary coverage: {path} matches no configured zone"),
589 severity: level,
590 category: "Bug Risk",
591 path: &path,
592 begin_line: line,
593 fingerprint: &fp,
594 }));
595 }
596}
597
598fn push_boundary_call_issues(
599 issues: &mut Vec<CodeClimateIssue>,
600 violations: &[fallow_types::output_dead_code::BoundaryCallViolationFinding],
601 root: &Path,
602 severity: Severity,
603) {
604 if violations.is_empty() {
605 return;
606 }
607 let level = severity_to_codeclimate(severity);
608 for entry in violations {
609 let v = &entry.violation;
610 let path = cc_path(&v.path, root);
611 let fp = fingerprint_hash(&["fallow/boundary-call-violation", &path, &v.callee]);
612 let line = if v.line > 0 { Some(v.line) } else { None };
613 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
614 check_name: "fallow/boundary-call-violation",
615 description: &format!(
616 "Boundary call: `{}` matches forbidden pattern `{}` in zone '{}'",
617 v.callee, v.pattern, v.zone
618 ),
619 severity: level,
620 category: "Bug Risk",
621 path: &path,
622 begin_line: line,
623 fingerprint: &fp,
624 }));
625 }
626}
627
628fn push_policy_violation_issues(
629 issues: &mut Vec<CodeClimateIssue>,
630 violations: &[fallow_types::output_dead_code::PolicyViolationFinding],
631 root: &Path,
632) {
633 use fallow_types::results::PolicyViolationSeverity;
634
635 for entry in violations {
636 let v = &entry.violation;
637 let path = cc_path(&v.path, root);
638 let rule = format!("{}/{}", v.pack, v.rule_id);
639 let fp = fingerprint_hash(&["fallow/policy-violation", &path, &rule, &v.matched]);
640 let line = if v.line > 0 { Some(v.line) } else { None };
641 let level = severity_to_codeclimate(match v.severity {
645 PolicyViolationSeverity::Error => Severity::Error,
646 PolicyViolationSeverity::Warn => Severity::Warn,
647 });
648 let message = match &v.message {
649 Some(message) => format!(
650 "Policy violation: `{}` is banned by `{rule}`. {message}",
651 v.matched
652 ),
653 None => format!("Policy violation: `{}` is banned by `{rule}`", v.matched),
654 };
655 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
656 check_name: "fallow/policy-violation",
657 description: &message,
658 severity: level,
659 category: "Bug Risk",
660 path: &path,
661 begin_line: line,
662 fingerprint: &fp,
663 }));
664 }
665}
666
667fn push_invalid_client_export_issues(
668 issues: &mut Vec<CodeClimateIssue>,
669 findings: &[fallow_types::output_dead_code::InvalidClientExportFinding],
670 root: &Path,
671 severity: Severity,
672) {
673 if findings.is_empty() {
674 return;
675 }
676 let level = severity_to_codeclimate(severity);
677 for entry in findings {
678 let e = &entry.export;
679 let path = cc_path(&e.path, root);
680 let fp = fingerprint_hash(&["fallow/invalid-client-export", &path, &e.export_name]);
681 let line = if e.line > 0 { Some(e.line) } else { None };
682 let message = format!(
683 "Export `{}` is not allowed in a \"{}\" file (Next.js server-only / route-config name)",
684 e.export_name, e.directive
685 );
686 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
687 check_name: "fallow/invalid-client-export",
688 description: &message,
689 severity: level,
690 category: "Bug Risk",
691 path: &path,
692 begin_line: line,
693 fingerprint: &fp,
694 }));
695 }
696}
697
698fn push_mixed_client_server_barrel_issues(
699 issues: &mut Vec<CodeClimateIssue>,
700 findings: &[fallow_types::output_dead_code::MixedClientServerBarrelFinding],
701 root: &Path,
702 severity: Severity,
703) {
704 if findings.is_empty() {
705 return;
706 }
707 let level = severity_to_codeclimate(severity);
708 for entry in findings {
709 let b = &entry.barrel;
710 let path = cc_path(&b.path, root);
711 let fp = fingerprint_hash(&[
712 "fallow/mixed-client-server-barrel",
713 &path,
714 &b.client_origin,
715 &b.server_origin,
716 ]);
717 let line = if b.line > 0 { Some(b.line) } else { None };
718 let message = format!(
719 "Barrel re-exports both a \"use client\" module (`{}`) and a server-only module (`{}`); one import drags the other's directive across the boundary",
720 b.client_origin, b.server_origin
721 );
722 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
723 check_name: "fallow/mixed-client-server-barrel",
724 description: &message,
725 severity: level,
726 category: "Bug Risk",
727 path: &path,
728 begin_line: line,
729 fingerprint: &fp,
730 }));
731 }
732}
733
734fn push_misplaced_directive_issues(
735 issues: &mut Vec<CodeClimateIssue>,
736 findings: &[fallow_types::output_dead_code::MisplacedDirectiveFinding],
737 root: &Path,
738 severity: Severity,
739) {
740 if findings.is_empty() {
741 return;
742 }
743 let level = severity_to_codeclimate(severity);
744 for entry in findings {
745 let d = &entry.directive_site;
746 let path = cc_path(&d.path, root);
747 let fp = fingerprint_hash(&[
748 "fallow/misplaced-directive",
749 &path,
750 &d.line.to_string(),
751 &d.directive,
752 ]);
753 let line = if d.line > 0 { Some(d.line) } else { None };
754 let message = format!(
755 "Directive `\"{}\"` is not in the leading position, so the RSC bundler ignores it; move it to the top of the file",
756 d.directive
757 );
758 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
759 check_name: "fallow/misplaced-directive",
760 description: &message,
761 severity: level,
762 category: "Bug Risk",
763 path: &path,
764 begin_line: line,
765 fingerprint: &fp,
766 }));
767 }
768}
769
770fn push_unprovided_inject_issues(
771 issues: &mut Vec<CodeClimateIssue>,
772 findings: &[fallow_types::output_dead_code::UnprovidedInjectFinding],
773 root: &Path,
774 severity: Severity,
775) {
776 if findings.is_empty() {
777 return;
778 }
779 let level = severity_to_codeclimate(severity);
780 for entry in findings {
781 let i = &entry.inject;
782 let path = cc_path(&i.path, root);
783 let fp = fingerprint_hash(&[
784 "fallow/unprovided-inject",
785 &path,
786 &i.line.to_string(),
787 &i.key_name,
788 ]);
789 let line = if i.line > 0 { Some(i.line) } else { None };
790 let message = format!(
791 "inject(`{}`) has no matching provide(`{}`) in this project; at runtime it returns undefined (provide the key or remove this inject)",
792 i.key_name, i.key_name
793 );
794 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
795 check_name: "fallow/unprovided-inject",
796 description: &message,
797 severity: level,
798 category: "Bug Risk",
799 path: &path,
800 begin_line: line,
801 fingerprint: &fp,
802 }));
803 }
804}
805
806fn push_unrendered_component_issues(
807 issues: &mut Vec<CodeClimateIssue>,
808 findings: &[fallow_types::output_dead_code::UnrenderedComponentFinding],
809 root: &Path,
810 severity: Severity,
811) {
812 if findings.is_empty() {
813 return;
814 }
815 let level = severity_to_codeclimate(severity);
816 for entry in findings {
817 let c = &entry.component;
818 let path = cc_path(&c.path, root);
819 let fp = fingerprint_hash(&[
820 "fallow/unrendered-component",
821 &path,
822 &c.line.to_string(),
823 &c.component_name,
824 ]);
825 let line = if c.line > 0 { Some(c.line) } else { None };
826 let message = format!(
827 "component `{}` is reachable but rendered nowhere in this project (render it somewhere or remove it)",
828 c.component_name
829 );
830 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
831 check_name: "fallow/unrendered-component",
832 description: &message,
833 severity: level,
834 category: "Bug Risk",
835 path: &path,
836 begin_line: line,
837 fingerprint: &fp,
838 }));
839 }
840}
841
842fn push_unused_component_prop_issues(
843 issues: &mut Vec<CodeClimateIssue>,
844 findings: &[fallow_types::output_dead_code::UnusedComponentPropFinding],
845 root: &Path,
846 severity: Severity,
847) {
848 if findings.is_empty() {
849 return;
850 }
851 let level = severity_to_codeclimate(severity);
852 for entry in findings {
853 let p = &entry.prop;
854 let path = cc_path(&p.path, root);
855 let fp = fingerprint_hash(&[
856 "fallow/unused-component-prop",
857 &path,
858 &p.line.to_string(),
859 &p.prop_name,
860 ]);
861 let line = if p.line > 0 { Some(p.line) } else { None };
862 let message = format!(
863 "prop `{}` is declared but referenced nowhere in component `{}` (remove it or use it)",
864 p.prop_name, p.component_name
865 );
866 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
867 check_name: "fallow/unused-component-prop",
868 description: &message,
869 severity: level,
870 category: "Bug Risk",
871 path: &path,
872 begin_line: line,
873 fingerprint: &fp,
874 }));
875 }
876}
877
878fn push_unused_component_emit_issues(
879 issues: &mut Vec<CodeClimateIssue>,
880 findings: &[fallow_types::output_dead_code::UnusedComponentEmitFinding],
881 root: &Path,
882 severity: Severity,
883) {
884 if findings.is_empty() {
885 return;
886 }
887 let level = severity_to_codeclimate(severity);
888 for entry in findings {
889 let e = &entry.emit;
890 let path = cc_path(&e.path, root);
891 let fp = fingerprint_hash(&[
892 "fallow/unused-component-emit",
893 &path,
894 &e.line.to_string(),
895 &e.emit_name,
896 ]);
897 let line = if e.line > 0 { Some(e.line) } else { None };
898 let message = format!(
899 "emit `{}` is declared but emitted nowhere in component `{}` (remove it or emit it)",
900 e.emit_name, e.component_name
901 );
902 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
903 check_name: "fallow/unused-component-emit",
904 description: &message,
905 severity: level,
906 category: "Bug Risk",
907 path: &path,
908 begin_line: line,
909 fingerprint: &fp,
910 }));
911 }
912}
913
914fn push_unused_svelte_event_issues(
915 issues: &mut Vec<CodeClimateIssue>,
916 findings: &[fallow_types::output_dead_code::UnusedSvelteEventFinding],
917 root: &Path,
918 severity: Severity,
919) {
920 if findings.is_empty() {
921 return;
922 }
923 let level = severity_to_codeclimate(severity);
924 for entry in findings {
925 let e = &entry.event;
926 let path = cc_path(&e.path, root);
927 let fp = fingerprint_hash(&[
928 "fallow/unused-svelte-event",
929 &path,
930 &e.line.to_string(),
931 &e.event_name,
932 ]);
933 let line = if e.line > 0 { Some(e.line) } else { None };
934 let message = format!(
935 "event `{}` is dispatched by component `{}` but listened to nowhere in the project (remove it or listen for it)",
936 e.event_name, e.component_name
937 );
938 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
939 check_name: "fallow/unused-svelte-event",
940 description: &message,
941 severity: level,
942 category: "Bug Risk",
943 path: &path,
944 begin_line: line,
945 fingerprint: &fp,
946 }));
947 }
948}
949
950fn push_unused_component_input_issues(
951 issues: &mut Vec<CodeClimateIssue>,
952 findings: &[fallow_types::output_dead_code::UnusedComponentInputFinding],
953 root: &Path,
954 severity: Severity,
955) {
956 if findings.is_empty() {
957 return;
958 }
959 let level = severity_to_codeclimate(severity);
960 for entry in findings {
961 let i = &entry.input;
962 let path = cc_path(&i.path, root);
963 let fp = fingerprint_hash(&[
964 "fallow/unused-component-input",
965 &path,
966 &i.line.to_string(),
967 &i.input_name,
968 ]);
969 let line = if i.line > 0 { Some(i.line) } else { None };
970 let message = format!(
971 "input `{}` is declared but referenced nowhere in component `{}` (remove it or use it)",
972 i.input_name, i.component_name
973 );
974 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
975 check_name: "fallow/unused-component-input",
976 description: &message,
977 severity: level,
978 category: "Bug Risk",
979 path: &path,
980 begin_line: line,
981 fingerprint: &fp,
982 }));
983 }
984}
985
986fn push_unused_component_output_issues(
987 issues: &mut Vec<CodeClimateIssue>,
988 findings: &[fallow_types::output_dead_code::UnusedComponentOutputFinding],
989 root: &Path,
990 severity: Severity,
991) {
992 if findings.is_empty() {
993 return;
994 }
995 let level = severity_to_codeclimate(severity);
996 for entry in findings {
997 let o = &entry.output;
998 let path = cc_path(&o.path, root);
999 let fp = fingerprint_hash(&[
1000 "fallow/unused-component-output",
1001 &path,
1002 &o.line.to_string(),
1003 &o.output_name,
1004 ]);
1005 let line = if o.line > 0 { Some(o.line) } else { None };
1006 let message = format!(
1007 "output `{}` is declared but emitted nowhere in component `{}` (remove it or emit it)",
1008 o.output_name, o.component_name
1009 );
1010 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1011 check_name: "fallow/unused-component-output",
1012 description: &message,
1013 severity: level,
1014 category: "Bug Risk",
1015 path: &path,
1016 begin_line: line,
1017 fingerprint: &fp,
1018 }));
1019 }
1020}
1021
1022fn push_unused_server_action_issues(
1023 issues: &mut Vec<CodeClimateIssue>,
1024 findings: &[fallow_types::output_dead_code::UnusedServerActionFinding],
1025 root: &Path,
1026 severity: Severity,
1027) {
1028 if findings.is_empty() {
1029 return;
1030 }
1031 let level = severity_to_codeclimate(severity);
1032 for entry in findings {
1033 let a = &entry.action;
1034 let path = cc_path(&a.path, root);
1035 let fp = fingerprint_hash(&[
1036 "fallow/unused-server-action",
1037 &path,
1038 &a.line.to_string(),
1039 &a.action_name,
1040 ]);
1041 let line = if a.line > 0 { Some(a.line) } else { None };
1042 let message = format!(
1043 "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)",
1044 a.action_name
1045 );
1046 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1047 check_name: "fallow/unused-server-action",
1048 description: &message,
1049 severity: level,
1050 category: "Bug Risk",
1051 path: &path,
1052 begin_line: line,
1053 fingerprint: &fp,
1054 }));
1055 }
1056}
1057
1058fn push_unused_load_data_key_issues(
1059 issues: &mut Vec<CodeClimateIssue>,
1060 findings: &[fallow_types::output_dead_code::UnusedLoadDataKeyFinding],
1061 root: &Path,
1062 severity: Severity,
1063) {
1064 if findings.is_empty() {
1065 return;
1066 }
1067 let level = severity_to_codeclimate(severity);
1068 for entry in findings {
1069 let k = &entry.key;
1070 let path = cc_path(&k.path, root);
1071 let fp = fingerprint_hash(&[
1072 "fallow/unused-load-data-key",
1073 &path,
1074 &k.line.to_string(),
1075 &k.key_name,
1076 ]);
1077 let line = if k.line > 0 { Some(k.line) } else { None };
1078 let message = format!(
1079 "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",
1080 k.key_name
1081 );
1082 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1083 check_name: "fallow/unused-load-data-key",
1084 description: &message,
1085 severity: level,
1086 category: "Bug Risk",
1087 path: &path,
1088 begin_line: line,
1089 fingerprint: &fp,
1090 }));
1091 }
1092}
1093
1094fn push_route_collision_issues(
1095 issues: &mut Vec<CodeClimateIssue>,
1096 findings: &[fallow_types::output_dead_code::RouteCollisionFinding],
1097 root: &Path,
1098 severity: Severity,
1099) {
1100 if findings.is_empty() {
1101 return;
1102 }
1103 let level = severity_to_codeclimate(severity);
1104 for entry in findings {
1105 let c = &entry.collision;
1106 let path = cc_path(&c.path, root);
1107 let fp = fingerprint_hash(&["fallow/route-collision", &path, &c.url]);
1108 let line = if c.line > 0 { Some(c.line) } else { None };
1109 let message = format!(
1110 "Route file resolves to `{}`, also owned by {} other file(s); Next.js fails the build because a URL can have only one owner",
1111 c.url,
1112 c.conflicting_paths.len()
1113 );
1114 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1115 check_name: "fallow/route-collision",
1116 description: &message,
1117 severity: level,
1118 category: "Bug Risk",
1119 path: &path,
1120 begin_line: line,
1121 fingerprint: &fp,
1122 }));
1123 }
1124}
1125
1126fn push_dynamic_segment_name_conflict_issues(
1127 issues: &mut Vec<CodeClimateIssue>,
1128 findings: &[fallow_types::output_dead_code::DynamicSegmentNameConflictFinding],
1129 root: &Path,
1130 severity: Severity,
1131) {
1132 if findings.is_empty() {
1133 return;
1134 }
1135 let level = severity_to_codeclimate(severity);
1136 for entry in findings {
1137 let c = &entry.conflict;
1138 let path = cc_path(&c.path, root);
1139 let fp = fingerprint_hash(&["fallow/dynamic-segment-name-conflict", &path, &c.position]);
1140 let line = if c.line > 0 { Some(c.line) } else { None };
1141 let message = format!(
1142 "Dynamic segments at `{}` use different slug names ({}); Next.js requires one consistent name per dynamic path",
1143 c.position,
1144 c.conflicting_segments.join(", ")
1145 );
1146 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1147 check_name: "fallow/dynamic-segment-name-conflict",
1148 description: &message,
1149 severity: level,
1150 category: "Bug Risk",
1151 path: &path,
1152 begin_line: line,
1153 fingerprint: &fp,
1154 }));
1155 }
1156}
1157
1158fn push_stale_suppression_issues(
1159 issues: &mut Vec<CodeClimateIssue>,
1160 suppressions: &[fallow_types::results::StaleSuppression],
1161 root: &Path,
1162 rules: &RulesConfig,
1163) {
1164 if suppressions.is_empty() {
1165 return;
1166 }
1167 for s in suppressions {
1168 let severity = if s.missing_reason {
1169 rules.require_suppression_reason
1170 } else {
1171 rules.stale_suppressions
1172 };
1173 let level = severity_to_codeclimate(severity);
1174 let path = cc_path(&s.path, root);
1175 let line_str = s.line.to_string();
1176 let check_name = if s.missing_reason {
1177 "fallow/missing-suppression-reason"
1178 } else {
1179 "fallow/stale-suppression"
1180 };
1181 let fp = fingerprint_hash(&[check_name, &path, &line_str]);
1182 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1183 check_name,
1184 description: &s.display_message(),
1185 severity: level,
1186 category: "Bug Risk",
1187 path: &path,
1188 begin_line: Some(s.line),
1189 fingerprint: &fp,
1190 }));
1191 }
1192}
1193
1194fn push_unused_catalog_entry_issues(
1195 issues: &mut Vec<CodeClimateIssue>,
1196 entries: &[fallow_types::output_dead_code::UnusedCatalogEntryFinding],
1197 root: &Path,
1198 severity: Severity,
1199) {
1200 if entries.is_empty() {
1201 return;
1202 }
1203 let level = severity_to_codeclimate(severity);
1204 for entry in entries {
1205 let entry = &entry.entry;
1206 let path = cc_path(&entry.path, root);
1207 let line_str = entry.line.to_string();
1208 let fp = fingerprint_hash(&[
1209 "fallow/unused-catalog-entry",
1210 &path,
1211 &line_str,
1212 &entry.catalog_name,
1213 &entry.entry_name,
1214 ]);
1215 let description = if entry.catalog_name == "default" {
1216 format!(
1217 "Catalog entry '{}' is not referenced by any workspace package",
1218 entry.entry_name
1219 )
1220 } else {
1221 format!(
1222 "Catalog entry '{}' (catalog '{}') is not referenced by any workspace package",
1223 entry.entry_name, entry.catalog_name
1224 )
1225 };
1226 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1227 check_name: "fallow/unused-catalog-entry",
1228 description: &description,
1229 severity: level,
1230 category: "Bug Risk",
1231 path: &path,
1232 begin_line: Some(entry.line),
1233 fingerprint: &fp,
1234 }));
1235 }
1236}
1237
1238fn push_unresolved_catalog_reference_issues(
1239 issues: &mut Vec<CodeClimateIssue>,
1240 findings: &[fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding],
1241 root: &Path,
1242 severity: Severity,
1243) {
1244 if findings.is_empty() {
1245 return;
1246 }
1247 let level = severity_to_codeclimate(severity);
1248 for finding in findings {
1249 let finding = &finding.reference;
1250 let path = cc_path(&finding.path, root);
1251 let line_str = finding.line.to_string();
1252 let fp = fingerprint_hash(&[
1253 "fallow/unresolved-catalog-reference",
1254 &path,
1255 &line_str,
1256 &finding.catalog_name,
1257 &finding.entry_name,
1258 ]);
1259 let catalog_phrase = if finding.catalog_name == "default" {
1260 "the default catalog".to_string()
1261 } else {
1262 format!("catalog '{}'", finding.catalog_name)
1263 };
1264 let mut description = format!(
1265 "Package '{}' is referenced via `catalog:{}` but {} does not declare it; `pnpm install` will fail",
1266 finding.entry_name,
1267 if finding.catalog_name == "default" {
1268 ""
1269 } else {
1270 finding.catalog_name.as_str()
1271 },
1272 catalog_phrase,
1273 );
1274 if !finding.available_in_catalogs.is_empty() {
1275 use std::fmt::Write as _;
1276 let _ = write!(
1277 description,
1278 " (available in: {})",
1279 finding.available_in_catalogs.join(", ")
1280 );
1281 }
1282 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1283 check_name: "fallow/unresolved-catalog-reference",
1284 description: &description,
1285 severity: level,
1286 category: "Bug Risk",
1287 path: &path,
1288 begin_line: Some(finding.line),
1289 fingerprint: &fp,
1290 }));
1291 }
1292}
1293
1294fn push_empty_catalog_group_issues(
1295 issues: &mut Vec<CodeClimateIssue>,
1296 groups: &[fallow_types::output_dead_code::EmptyCatalogGroupFinding],
1297 root: &Path,
1298 severity: Severity,
1299) {
1300 if groups.is_empty() {
1301 return;
1302 }
1303 let level = severity_to_codeclimate(severity);
1304 for group in groups {
1305 let group = &group.group;
1306 let path = cc_path(&group.path, root);
1307 let line_str = group.line.to_string();
1308 let fp = fingerprint_hash(&[
1309 "fallow/empty-catalog-group",
1310 &path,
1311 &line_str,
1312 &group.catalog_name,
1313 ]);
1314 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1315 check_name: "fallow/empty-catalog-group",
1316 description: &format!("Catalog group '{}' has no entries", group.catalog_name),
1317 severity: level,
1318 category: "Bug Risk",
1319 path: &path,
1320 begin_line: Some(group.line),
1321 fingerprint: &fp,
1322 }));
1323 }
1324}
1325
1326fn push_unused_dependency_override_issues(
1327 issues: &mut Vec<CodeClimateIssue>,
1328 findings: &[fallow_types::output_dead_code::UnusedDependencyOverrideFinding],
1329 root: &Path,
1330 severity: Severity,
1331) {
1332 if findings.is_empty() {
1333 return;
1334 }
1335 let level = severity_to_codeclimate(severity);
1336 for finding in findings {
1337 let finding = &finding.entry;
1338 let path = cc_path(&finding.path, root);
1339 let line_str = finding.line.to_string();
1340 let fp = fingerprint_hash(&[
1341 "fallow/unused-dependency-override",
1342 &path,
1343 &line_str,
1344 finding.source.as_label(),
1345 &finding.raw_key,
1346 ]);
1347 let mut description = format!(
1348 "Override `{}` forces version `{}` but `{}` is not declared by any workspace package or resolved in the lockfile",
1349 finding.raw_key, finding.version_range, finding.target_package,
1350 );
1351 if let Some(hint) = &finding.hint {
1352 use std::fmt::Write as _;
1353 let _ = write!(description, " ({hint})");
1354 }
1355 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1356 check_name: "fallow/unused-dependency-override",
1357 description: &description,
1358 severity: level,
1359 category: "Bug Risk",
1360 path: &path,
1361 begin_line: Some(finding.line),
1362 fingerprint: &fp,
1363 }));
1364 }
1365}
1366
1367fn push_misconfigured_dependency_override_issues(
1368 issues: &mut Vec<CodeClimateIssue>,
1369 findings: &[fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding],
1370 root: &Path,
1371 severity: Severity,
1372) {
1373 if findings.is_empty() {
1374 return;
1375 }
1376 let level = severity_to_codeclimate(severity);
1377 for finding in findings {
1378 let finding = &finding.entry;
1379 let path = cc_path(&finding.path, root);
1380 let line_str = finding.line.to_string();
1381 let fp = fingerprint_hash(&[
1382 "fallow/misconfigured-dependency-override",
1383 &path,
1384 &line_str,
1385 finding.source.as_label(),
1386 &finding.raw_key,
1387 ]);
1388 let description = format!(
1389 "Override `{}` -> `{}` is malformed: {}",
1390 finding.raw_key,
1391 finding.raw_value,
1392 finding.reason.describe(),
1393 );
1394 issues.push(build_codeclimate_issue(CodeClimateIssueInput {
1395 check_name: "fallow/misconfigured-dependency-override",
1396 description: &description,
1397 severity: level,
1398 category: "Bug Risk",
1399 path: &path,
1400 begin_line: Some(finding.line),
1401 fingerprint: &fp,
1402 }));
1403 }
1404}
1405
1406#[must_use]
1413pub fn build_codeclimate(
1414 results: &AnalysisResults,
1415 root: &Path,
1416 rules: &RulesConfig,
1417) -> Vec<CodeClimateIssue> {
1418 CodeClimateBuilder {
1419 issues: Vec::new(),
1420 results,
1421 root,
1422 rules,
1423 }
1424 .build()
1425}
1426
1427struct CodeClimateBuilder<'a> {
1428 issues: Vec<CodeClimateIssue>,
1429 results: &'a AnalysisResults,
1430 root: &'a Path,
1431 rules: &'a RulesConfig,
1432}
1433
1434impl CodeClimateBuilder<'_> {
1435 fn build(mut self) -> Vec<CodeClimateIssue> {
1436 self.push_file_and_export_issues();
1437 self.push_private_type_leak_issues();
1438 self.push_package_dependency_issues();
1439 self.push_type_test_dependency_issues();
1440 self.push_member_issues();
1441 self.push_import_and_duplicate_issues();
1442 self.push_graph_issues();
1443 self.push_boundary_issues();
1444 self.push_suppression_and_catalog_issues();
1445 self.push_override_issues();
1446 self.issues
1447 }
1448
1449 fn push_file_and_export_issues(&mut self) {
1450 push_unused_file_issues(
1451 &mut self.issues,
1452 &self.results.unused_files,
1453 self.root,
1454 self.rules.unused_files,
1455 );
1456 push_unused_export_issues(UnusedExportIssuesInput {
1457 issues: &mut self.issues,
1458 exports: self
1459 .results
1460 .unused_exports
1461 .iter()
1462 .map(|e| (&e.export, e.reachability_caveats.as_slice())),
1463 root: self.root,
1464 rule_id: "fallow/unused-export",
1465 direct_label: "Export",
1466 re_export_label: "Re-export",
1467 severity: self.rules.unused_exports,
1468 });
1469 push_unused_export_issues(UnusedExportIssuesInput {
1470 issues: &mut self.issues,
1471 exports: self
1472 .results
1473 .unused_types
1474 .iter()
1475 .map(|e| (&e.export, e.reachability_caveats.as_slice())),
1476 root: self.root,
1477 rule_id: "fallow/unused-type",
1478 direct_label: "Type export",
1479 re_export_label: "Type re-export",
1480 severity: self.rules.unused_types,
1481 });
1482 }
1483
1484 fn push_private_type_leak_issues(&mut self) {
1485 push_private_type_leak_issues(
1486 &mut self.issues,
1487 &self.results.private_type_leaks,
1488 self.root,
1489 self.rules.private_type_leaks,
1490 );
1491 }
1492
1493 fn push_package_dependency_issues(&mut self) {
1494 push_dep_cc_issues(
1495 &mut self.issues,
1496 self.results
1497 .unused_dependencies
1498 .iter()
1499 .map(|f| (&f.dep, f.reachability_caveats.as_slice())),
1500 self.root,
1501 "fallow/unused-dependency",
1502 "dependencies",
1503 self.rules.unused_dependencies,
1504 );
1505 push_dep_cc_issues(
1506 &mut self.issues,
1507 self.results
1508 .unused_dev_dependencies
1509 .iter()
1510 .map(|f| (&f.dep, f.reachability_caveats.as_slice())),
1511 self.root,
1512 "fallow/unused-dev-dependency",
1513 "devDependencies",
1514 self.rules.unused_dev_dependencies,
1515 );
1516 push_dep_cc_issues(
1517 &mut self.issues,
1518 self.results
1519 .unused_optional_dependencies
1520 .iter()
1521 .map(|f| (&f.dep, f.reachability_caveats.as_slice())),
1522 self.root,
1523 "fallow/unused-optional-dependency",
1524 "optionalDependencies",
1525 self.rules.unused_optional_dependencies,
1526 );
1527 }
1528
1529 fn push_type_test_dependency_issues(&mut self) {
1530 push_type_only_dep_issues(
1531 &mut self.issues,
1532 &self.results.type_only_dependencies,
1533 self.root,
1534 self.rules.type_only_dependencies,
1535 );
1536 push_test_only_dep_issues(
1537 &mut self.issues,
1538 &self.results.test_only_dependencies,
1539 self.root,
1540 self.rules.test_only_dependencies,
1541 );
1542 push_dev_dep_in_prod_issues(
1543 &mut self.issues,
1544 &self.results.dev_dependencies_in_production,
1545 self.root,
1546 self.rules.dev_dependencies_in_production,
1547 );
1548 }
1549
1550 fn push_member_issues(&mut self) {
1551 push_unused_member_issues(
1552 &mut self.issues,
1553 self.results
1554 .unused_enum_members
1555 .iter()
1556 .map(|m| (&m.member, m.reachability_caveats.as_slice())),
1557 self.root,
1558 "fallow/unused-enum-member",
1559 "Enum",
1560 self.rules.unused_enum_members,
1561 );
1562 push_unused_member_issues(
1563 &mut self.issues,
1564 self.results
1565 .unused_class_members
1566 .iter()
1567 .map(|m| (&m.member, m.reachability_caveats.as_slice())),
1568 self.root,
1569 "fallow/unused-class-member",
1570 "Class",
1571 self.rules.unused_class_members,
1572 );
1573 push_unused_member_issues(
1574 &mut self.issues,
1575 self.results
1576 .unused_store_members
1577 .iter()
1578 .map(|m| (&m.member, m.reachability_caveats.as_slice())),
1579 self.root,
1580 "fallow/unused-store-member",
1581 "Store",
1582 self.rules.unused_store_members,
1583 );
1584 }
1585
1586 fn push_import_and_duplicate_issues(&mut self) {
1587 push_unresolved_import_issues(
1588 &mut self.issues,
1589 &self.results.unresolved_imports,
1590 self.root,
1591 self.rules.unresolved_imports,
1592 );
1593 push_unlisted_dep_issues(
1594 &mut self.issues,
1595 &self.results.unlisted_dependencies,
1596 self.root,
1597 self.rules.unlisted_dependencies,
1598 );
1599 push_duplicate_export_issues(
1600 &mut self.issues,
1601 &self.results.duplicate_exports,
1602 self.root,
1603 self.rules.duplicate_exports,
1604 );
1605 }
1606
1607 fn push_graph_issues(&mut self) {
1608 push_circular_dep_issues(
1609 &mut self.issues,
1610 &self.results.circular_dependencies,
1611 self.root,
1612 self.rules.circular_dependencies,
1613 );
1614 push_re_export_cycle_issues(
1615 &mut self.issues,
1616 &self.results.re_export_cycles,
1617 self.root,
1618 self.rules.re_export_cycle,
1619 );
1620 }
1621
1622 fn push_boundary_issues(&mut self) {
1623 self.push_architecture_boundary_issues();
1624 self.push_client_server_boundary_issues();
1625 self.push_component_boundary_issues();
1626 self.push_framework_route_issues();
1627 }
1628
1629 fn push_architecture_boundary_issues(&mut self) {
1630 push_boundary_violation_issues(
1631 &mut self.issues,
1632 &self.results.boundary_violations,
1633 self.root,
1634 self.rules.boundary_violation,
1635 );
1636 push_boundary_coverage_issues(
1637 &mut self.issues,
1638 &self.results.boundary_coverage_violations,
1639 self.root,
1640 self.rules.boundary_violation,
1641 );
1642 push_boundary_call_issues(
1643 &mut self.issues,
1644 &self.results.boundary_call_violations,
1645 self.root,
1646 self.rules.boundary_violation,
1647 );
1648 push_policy_violation_issues(&mut self.issues, &self.results.policy_violations, self.root);
1649 }
1650
1651 fn push_client_server_boundary_issues(&mut self) {
1652 push_invalid_client_export_issues(
1653 &mut self.issues,
1654 &self.results.invalid_client_exports,
1655 self.root,
1656 self.rules.invalid_client_export,
1657 );
1658 push_mixed_client_server_barrel_issues(
1659 &mut self.issues,
1660 &self.results.mixed_client_server_barrels,
1661 self.root,
1662 self.rules.mixed_client_server_barrel,
1663 );
1664 push_misplaced_directive_issues(
1665 &mut self.issues,
1666 &self.results.misplaced_directives,
1667 self.root,
1668 self.rules.misplaced_directive,
1669 );
1670 }
1671
1672 fn push_component_boundary_issues(&mut self) {
1673 push_unprovided_inject_issues(
1674 &mut self.issues,
1675 &self.results.unprovided_injects,
1676 self.root,
1677 self.rules.unprovided_injects,
1678 );
1679 push_unrendered_component_issues(
1680 &mut self.issues,
1681 &self.results.unrendered_components,
1682 self.root,
1683 self.rules.unrendered_components,
1684 );
1685 push_unused_component_prop_issues(
1686 &mut self.issues,
1687 &self.results.unused_component_props,
1688 self.root,
1689 self.rules.unused_component_props,
1690 );
1691 push_unused_component_emit_issues(
1692 &mut self.issues,
1693 &self.results.unused_component_emits,
1694 self.root,
1695 self.rules.unused_component_emits,
1696 );
1697 push_unused_component_input_issues(
1698 &mut self.issues,
1699 &self.results.unused_component_inputs,
1700 self.root,
1701 self.rules.unused_component_inputs,
1702 );
1703 push_unused_component_output_issues(
1704 &mut self.issues,
1705 &self.results.unused_component_outputs,
1706 self.root,
1707 self.rules.unused_component_outputs,
1708 );
1709 push_unused_svelte_event_issues(
1710 &mut self.issues,
1711 &self.results.unused_svelte_events,
1712 self.root,
1713 self.rules.unused_svelte_events,
1714 );
1715 }
1716
1717 fn push_framework_route_issues(&mut self) {
1718 push_unused_server_action_issues(
1719 &mut self.issues,
1720 &self.results.unused_server_actions,
1721 self.root,
1722 self.rules.unused_server_actions,
1723 );
1724 push_unused_load_data_key_issues(
1725 &mut self.issues,
1726 &self.results.unused_load_data_keys,
1727 self.root,
1728 self.rules.unused_load_data_keys,
1729 );
1730 push_route_collision_issues(
1731 &mut self.issues,
1732 &self.results.route_collisions,
1733 self.root,
1734 self.rules.route_collision,
1735 );
1736 push_dynamic_segment_name_conflict_issues(
1737 &mut self.issues,
1738 &self.results.dynamic_segment_name_conflicts,
1739 self.root,
1740 self.rules.dynamic_segment_name_conflict,
1741 );
1742 }
1743
1744 fn push_suppression_and_catalog_issues(&mut self) {
1745 push_stale_suppression_issues(
1746 &mut self.issues,
1747 &self.results.stale_suppressions,
1748 self.root,
1749 self.rules,
1750 );
1751 push_unused_catalog_entry_issues(
1752 &mut self.issues,
1753 &self.results.unused_catalog_entries,
1754 self.root,
1755 self.rules.unused_catalog_entries,
1756 );
1757 push_empty_catalog_group_issues(
1758 &mut self.issues,
1759 &self.results.empty_catalog_groups,
1760 self.root,
1761 self.rules.empty_catalog_groups,
1762 );
1763 push_unresolved_catalog_reference_issues(
1764 &mut self.issues,
1765 &self.results.unresolved_catalog_references,
1766 self.root,
1767 self.rules.unresolved_catalog_references,
1768 );
1769 }
1770
1771 fn push_override_issues(&mut self) {
1772 push_unused_dependency_override_issues(
1773 &mut self.issues,
1774 &self.results.unused_dependency_overrides,
1775 self.root,
1776 self.rules.unused_dependency_overrides,
1777 );
1778 push_misconfigured_dependency_override_issues(
1779 &mut self.issues,
1780 &self.results.misconfigured_dependency_overrides,
1781 self.root,
1782 self.rules.misconfigured_dependency_overrides,
1783 );
1784 }
1785}
1786
1787#[cfg(test)]
1788mod tests {
1789 use std::collections::BTreeSet;
1790
1791 use fallow_output::issue_output_contracts;
1792
1793 fn codeclimate_check_name_literals() -> BTreeSet<String> {
1794 let source = include_str!("dead_code_codeclimate.rs")
1795 .split("#[cfg(test)]")
1796 .next()
1797 .expect("source before tests");
1798 let mut literals = BTreeSet::new();
1799 let mut rest = source;
1800 while let Some(start) = rest.find("\"fallow/") {
1801 let after_quote = &rest[start + 1..];
1802 let Some(end) = after_quote.find('"') else {
1803 break;
1804 };
1805 literals.insert(after_quote[..end].to_owned());
1806 rest = &after_quote[end + 1..];
1807 }
1808 literals
1809 }
1810
1811 #[test]
1812 fn codeclimate_check_names_match_issue_contracts() {
1813 let from_emitter = codeclimate_check_name_literals();
1814 let from_contracts = issue_output_contracts()
1815 .flat_map(|contract| contract.codeclimate_check_names)
1816 .collect::<BTreeSet<_>>();
1817
1818 assert_eq!(from_emitter, from_contracts);
1819 }
1820
1821 mod caveats {
1822 use std::path::{Path, PathBuf};
1823
1824 use fallow_config::RulesConfig;
1825 use fallow_types::extract::MemberKind;
1826 use fallow_types::output_dead_code::{
1827 ReachabilityCaveat, UnusedClassMemberFinding, UnusedDependencyFinding,
1828 UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding,
1829 UnusedStoreMemberFinding,
1830 };
1831 use fallow_types::results::{
1832 AnalysisResults, DependencyLocation, UnusedDependency, UnusedExport, UnusedFile,
1833 UnusedMember,
1834 };
1835
1836 use crate::dead_code_codeclimate::build_codeclimate;
1837
1838 fn results_with(root: &Path, caveated: bool) -> AnalysisResults {
1840 let caveats = if caveated {
1841 vec![ReachabilityCaveat::IncompleteImportGraph]
1842 } else {
1843 Vec::new()
1844 };
1845 let mut results = AnalysisResults::default();
1846
1847 let mut file = UnusedFileFinding::with_actions(UnusedFile {
1848 path: root.join("src/lib.ts"),
1849 });
1850 file.reachability_caveats.clone_from(&caveats);
1851 results.unused_files.push(file);
1852
1853 let mut export = UnusedExportFinding::with_actions(UnusedExport {
1854 path: root.join("src/lib.ts"),
1855 export_name: "needed".to_owned(),
1856 is_type_only: false,
1857 line: 3,
1858 col: 0,
1859 span_start: 0,
1860 is_re_export: false,
1861 });
1862 export.reachability_caveats.clone_from(&caveats);
1863 results.unused_exports.push(export);
1864
1865 let mut dep = UnusedDependencyFinding::with_actions(UnusedDependency {
1866 package_name: "left-pad".to_owned(),
1867 location: DependencyLocation::Dependencies,
1868 path: root.join("package.json"),
1869 line: 5,
1870 used_in_workspaces: Vec::new(),
1871 });
1872 dep.reachability_caveats.clone_from(&caveats);
1873 results.unused_dependencies.push(dep);
1874
1875 let member = |parent: &str, name: &str, kind| UnusedMember {
1876 path: root.join("src/lib.ts"),
1877 parent_name: parent.to_owned(),
1878 member_name: name.to_owned(),
1879 kind,
1880 line: 7,
1881 col: 2,
1882 };
1883
1884 let mut enum_member = UnusedEnumMemberFinding::with_actions(member(
1885 "Mode",
1886 "Legacy",
1887 MemberKind::EnumMember,
1888 ));
1889 enum_member.reachability_caveats.clone_from(&caveats);
1890 results.unused_enum_members.push(enum_member);
1891
1892 let mut class_member = UnusedClassMemberFinding::with_actions(member(
1893 "Widget",
1894 "render",
1895 MemberKind::ClassMethod,
1896 ));
1897 class_member.reachability_caveats.clone_from(&caveats);
1898 results.unused_class_members.push(class_member);
1899
1900 let mut store_member = UnusedStoreMemberFinding::with_actions(member(
1901 "useCart",
1902 "subtotal",
1903 MemberKind::StoreMember,
1904 ));
1905 store_member.reachability_caveats.clone_from(&caveats);
1906 results.unused_store_members.push(store_member);
1907
1908 results
1909 }
1910
1911 #[test]
1916 fn descriptions_name_the_caveat() {
1917 let root = PathBuf::from("/project");
1918
1919 let issues =
1920 build_codeclimate(&results_with(&root, true), &root, &RulesConfig::default());
1921
1922 let descriptions: Vec<&str> = issues
1923 .iter()
1924 .map(|issue| issue.description.as_str())
1925 .collect();
1926 assert!(
1927 descriptions
1928 .iter()
1929 .all(|description| description.ends_with(" (caveat: incomplete import graph)")),
1930 "every caveated finding hedges its description: {descriptions:?}"
1931 );
1932 assert!(
1933 descriptions.contains(
1934 &"File is not reachable from any entry point (caveat: incomplete import graph)"
1935 ),
1936 "{descriptions:?}"
1937 );
1938 }
1939
1940 #[test]
1944 fn a_clean_run_carries_no_caveat_text() {
1945 let root = PathBuf::from("/project");
1946
1947 let issues =
1948 build_codeclimate(&results_with(&root, false), &root, &RulesConfig::default());
1949
1950 assert!(
1951 issues
1952 .iter()
1953 .all(|issue| !issue.description.contains("caveat")),
1954 "{:?}",
1955 issues
1956 .iter()
1957 .map(|issue| issue.description.as_str())
1958 .collect::<Vec<_>>()
1959 );
1960 }
1961
1962 #[test]
1966 fn the_caveat_does_not_move_the_fingerprint() {
1967 let root = PathBuf::from("/project");
1968
1969 let clean =
1970 build_codeclimate(&results_with(&root, false), &root, &RulesConfig::default());
1971 let caveated =
1972 build_codeclimate(&results_with(&root, true), &root, &RulesConfig::default());
1973
1974 let fingerprints = |issues: &[fallow_output::CodeClimateIssue]| {
1975 issues
1976 .iter()
1977 .map(|issue| issue.fingerprint.clone())
1978 .collect::<Vec<_>>()
1979 };
1980 assert_eq!(fingerprints(&clean), fingerprints(&caveated));
1981 assert_ne!(
1982 clean[0].description, caveated[0].description,
1983 "the guard is only meaningful while the description actually changed"
1984 );
1985 }
1986 }
1987}