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