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
243/// Push CodeClimate issues for unused enum or class members.
244///
245/// `entity_label` is `"Enum"` or `"Class"` so the rendered description reads
246/// "Enum member ..." or "Class member ..." accordingly.
247fn 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        // Severity comes from the EFFECTIVE per-finding value, not the
571        // policy-violation master, so a severity: "error" rule under a warn
572        // master maps to blocker-level just like the exit-code gate.
573        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/// Build CodeClimate issues from dead-code analysis results.
1336///
1337/// Returns the typed [`CodeClimateIssue`] vec; callers that emit the wire
1338/// shape convert via [`fallow_output::codeclimate_issues_to_value`]. The schema
1339/// drift gate locks the per-issue shape against
1340/// [`fallow_output::CodeClimateOutput`].
1341#[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}