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