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