Skip to main content

fallow_api/
dead_code_codeclimate.rs

1//! Shared dead-code CodeClimate issue construction.
2
3use 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
34/// Push CodeClimate issues for unused dependencies with a shared structure.
35fn 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
101/// Push CodeClimate issues for unused exports or unused types.
102///
103/// `direct_label` / `re_export_label` let the same helper produce the right
104/// prose for both `unused-export` (Export / Re-export) and `unused-type`
105/// (Type export / Type re-export) rule ids.
106struct 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
273/// Push CodeClimate issues for unused enum or class members.
274///
275/// `entity_label` is `"Enum"` or `"Class"` so the rendered description reads
276/// "Enum member ..." or "Class member ..." accordingly.
277fn 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        // Severity comes from the EFFECTIVE per-finding value, not the
601        // policy-violation master, so a severity: "error" rule under a warn
602        // master maps to blocker-level just like the exit-code gate.
603        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/// Build CodeClimate issues from dead-code analysis results.
1366///
1367/// Returns the typed [`CodeClimateIssue`] vec; callers that emit the wire
1368/// shape convert via [`fallow_output::codeclimate_issues_to_value`]. The schema
1369/// drift gate locks the per-issue shape against
1370/// [`fallow_output::CodeClimateOutput`].
1371#[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}