Skip to main content

fallow_types/
issue_meta.rs

1//! Shared issue-type contract metadata.
2
3use crate::suppress::IssueKind;
4
5/// Shared contract facts for issue-like diagnostics.
6///
7/// Curated prose stays with the surface that owns it. This table is only for
8/// stable machine-facing facts that otherwise drift across CLI schema, LSP,
9/// MCP, and suppression helpers.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct IssueKindMeta {
12    /// Backing suppression issue kind, when this row maps to one.
13    pub kind: Option<IssueKind>,
14    /// Canonical issue code used in output and diagnostic codes.
15    pub code: &'static str,
16    /// Accepted aliases for config, suppression, or migration compatibility.
17    pub aliases: &'static [&'static str],
18    /// User-facing label for editor and capability surfaces.
19    pub label: &'static str,
20    /// Canonical `[rules]` key when the issue is configurable.
21    pub config_key: Option<&'static str>,
22    /// Dead-code CLI filter flag, when one exists.
23    pub filter_flag: Option<&'static str>,
24    /// MCP `issue_types` selector, when this issue can be selected there.
25    pub mcp_issue_type: Option<&'static str>,
26    /// Suppression token agents should emit, when suppressible.
27    pub suppress_token: Option<&'static str>,
28    /// Whether the suppression comment should use `fallow-ignore-file`.
29    pub suppress_file_level: bool,
30    /// Whether the LSP exposes this row through initialization options and
31    /// `fallow/issueTypes`.
32    pub lsp: bool,
33    /// Broad documentation category for authoring and generated manifests.
34    pub docs_category: &'static str,
35}
36
37impl IssueKindMeta {
38    /// Return the filter flag as an MCP selector pair.
39    #[must_use]
40    pub const fn mcp_pair(self) -> Option<(&'static str, &'static str)> {
41        match (self.mcp_issue_type, self.filter_flag) {
42            (Some(issue_type), Some(flag)) => Some((issue_type, flag)),
43            _ => None,
44        }
45    }
46
47    /// Whether this row owns a serialized dead-code result contract.
48    #[must_use]
49    pub fn has_result_contract(self) -> bool {
50        issue_result_meta_by_code(self.code).is_some()
51    }
52
53    /// SARIF rule ids used by CI formatters for this issue row.
54    #[must_use]
55    pub fn sarif_rule_ids(self) -> Vec<String> {
56        issue_sarif_rule_ids(self.code)
57    }
58
59    /// Whether this issue row is eligible for SARIF rule metadata.
60    #[must_use]
61    pub fn sarif_enabled(self) -> bool {
62        self.has_result_contract()
63    }
64
65    /// CodeClimate check names used by CI formatters for this issue row.
66    #[must_use]
67    pub fn codeclimate_check_names(self) -> Vec<String> {
68        issue_codeclimate_check_names(self.code)
69    }
70
71    /// Whether this issue row is eligible for CodeClimate output.
72    #[must_use]
73    pub fn codeclimate_enabled(self) -> bool {
74        !self.codeclimate_check_names().is_empty()
75    }
76
77    /// Documentation anchor under `/explanations/dead-code`.
78    #[must_use]
79    pub fn docs_anchor(self) -> Option<&'static str> {
80        issue_docs_anchor(self.code)
81    }
82
83    /// Published TypeScript backwards-compat alias policy.
84    #[must_use]
85    pub fn ts_alias(self) -> Option<TsAliasMeta> {
86        issue_ts_alias(self.code)
87    }
88}
89
90/// All shared issue metadata rows.
91pub const ISSUE_KIND_META: &[IssueKindMeta] = &[
92    IssueKindMeta {
93        kind: Some(IssueKind::CodeDuplication),
94        code: "code-duplication",
95        aliases: &[],
96        label: "Code Duplication",
97        config_key: None,
98        filter_flag: None,
99        mcp_issue_type: None,
100        suppress_token: Some("code-duplication"),
101        suppress_file_level: false,
102        lsp: true,
103        docs_category: "dupes",
104    },
105    IssueKindMeta {
106        kind: Some(IssueKind::UnusedFile),
107        code: "unused-file",
108        aliases: &[],
109        label: "Unused Files",
110        config_key: Some("unused-files"),
111        filter_flag: Some("--unused-files"),
112        mcp_issue_type: Some("unused-files"),
113        suppress_token: Some("unused-file"),
114        suppress_file_level: true,
115        lsp: true,
116        docs_category: "source",
117    },
118    IssueKindMeta {
119        kind: Some(IssueKind::UnusedExport),
120        code: "unused-export",
121        aliases: &[],
122        label: "Unused Exports",
123        config_key: Some("unused-exports"),
124        filter_flag: Some("--unused-exports"),
125        mcp_issue_type: Some("unused-exports"),
126        suppress_token: Some("unused-export"),
127        suppress_file_level: false,
128        lsp: true,
129        docs_category: "source",
130    },
131    IssueKindMeta {
132        kind: Some(IssueKind::UnusedType),
133        code: "unused-type",
134        aliases: &[],
135        label: "Unused Types",
136        config_key: Some("unused-types"),
137        filter_flag: Some("--unused-types"),
138        mcp_issue_type: Some("unused-types"),
139        suppress_token: Some("unused-type"),
140        suppress_file_level: false,
141        lsp: true,
142        docs_category: "source",
143    },
144    IssueKindMeta {
145        kind: Some(IssueKind::PrivateTypeLeak),
146        code: "private-type-leak",
147        aliases: &[],
148        label: "Private Type Leaks",
149        config_key: Some("private-type-leaks"),
150        filter_flag: Some("--private-type-leaks"),
151        mcp_issue_type: Some("private-type-leaks"),
152        suppress_token: Some("private-type-leak"),
153        suppress_file_level: false,
154        lsp: true,
155        docs_category: "source",
156    },
157    IssueKindMeta {
158        kind: Some(IssueKind::UnusedDependency),
159        code: "unused-dependency",
160        aliases: &[],
161        label: "Unused Dependencies",
162        config_key: Some("unused-dependencies"),
163        filter_flag: Some("--unused-deps"),
164        mcp_issue_type: Some("unused-deps"),
165        suppress_token: None,
166        suppress_file_level: false,
167        lsp: true,
168        docs_category: "dependency",
169    },
170    IssueKindMeta {
171        kind: Some(IssueKind::UnusedDevDependency),
172        code: "unused-dev-dependency",
173        aliases: &[],
174        label: "Unused Dev Dependencies",
175        config_key: Some("unused-dev-dependencies"),
176        filter_flag: Some("--unused-deps"),
177        mcp_issue_type: None,
178        suppress_token: None,
179        suppress_file_level: false,
180        lsp: true,
181        docs_category: "dependency",
182    },
183    IssueKindMeta {
184        kind: None,
185        code: "unused-optional-dependency",
186        aliases: &[],
187        label: "Unused Optional Dependencies",
188        config_key: Some("unused-optional-dependencies"),
189        filter_flag: Some("--unused-deps"),
190        mcp_issue_type: None,
191        suppress_token: None,
192        suppress_file_level: false,
193        lsp: true,
194        docs_category: "dependency",
195    },
196    IssueKindMeta {
197        kind: Some(IssueKind::UnusedEnumMember),
198        code: "unused-enum-member",
199        aliases: &[],
200        label: "Unused Enum Members",
201        config_key: Some("unused-enum-members"),
202        filter_flag: Some("--unused-enum-members"),
203        mcp_issue_type: Some("unused-enum-members"),
204        suppress_token: Some("unused-enum-member"),
205        suppress_file_level: false,
206        lsp: true,
207        docs_category: "source",
208    },
209    IssueKindMeta {
210        kind: Some(IssueKind::UnusedClassMember),
211        code: "unused-class-member",
212        aliases: &[],
213        label: "Unused Class Members",
214        config_key: Some("unused-class-members"),
215        filter_flag: Some("--unused-class-members"),
216        mcp_issue_type: Some("unused-class-members"),
217        suppress_token: Some("unused-class-member"),
218        suppress_file_level: false,
219        lsp: true,
220        docs_category: "source",
221    },
222    IssueKindMeta {
223        kind: Some(IssueKind::UnusedStoreMember),
224        code: "unused-store-member",
225        aliases: &["unused-store-members"],
226        label: "Unused Store Members",
227        config_key: Some("unused-store-members"),
228        filter_flag: Some("--unused-store-members"),
229        mcp_issue_type: Some("unused-store-members"),
230        suppress_token: Some("unused-store-member"),
231        suppress_file_level: false,
232        lsp: true,
233        docs_category: "framework",
234    },
235    IssueKindMeta {
236        kind: Some(IssueKind::UnresolvedImport),
237        code: "unresolved-import",
238        aliases: &[],
239        label: "Unresolved Imports",
240        config_key: Some("unresolved-imports"),
241        filter_flag: Some("--unresolved-imports"),
242        mcp_issue_type: Some("unresolved-imports"),
243        suppress_token: Some("unresolved-import"),
244        suppress_file_level: false,
245        lsp: true,
246        docs_category: "dependency",
247    },
248    IssueKindMeta {
249        kind: Some(IssueKind::UnlistedDependency),
250        code: "unlisted-dependency",
251        aliases: &[],
252        label: "Unlisted Dependencies",
253        config_key: Some("unlisted-dependencies"),
254        filter_flag: Some("--unlisted-deps"),
255        mcp_issue_type: Some("unlisted-deps"),
256        suppress_token: None,
257        suppress_file_level: false,
258        lsp: true,
259        docs_category: "dependency",
260    },
261    IssueKindMeta {
262        kind: Some(IssueKind::DuplicateExport),
263        code: "duplicate-export",
264        aliases: &[],
265        label: "Duplicate Exports",
266        config_key: Some("duplicate-exports"),
267        filter_flag: Some("--duplicate-exports"),
268        mcp_issue_type: Some("duplicate-exports"),
269        suppress_token: Some("duplicate-export"),
270        suppress_file_level: true,
271        lsp: true,
272        docs_category: "source",
273    },
274    IssueKindMeta {
275        kind: Some(IssueKind::TypeOnlyDependency),
276        code: "type-only-dependency",
277        aliases: &[],
278        label: "Type-Only Dependencies",
279        config_key: Some("type-only-dependencies"),
280        filter_flag: Some("--unused-deps"),
281        mcp_issue_type: None,
282        suppress_token: None,
283        suppress_file_level: false,
284        lsp: true,
285        docs_category: "dependency",
286    },
287    IssueKindMeta {
288        kind: Some(IssueKind::TestOnlyDependency),
289        code: "test-only-dependency",
290        aliases: &[],
291        label: "Test-Only Dependencies",
292        config_key: Some("test-only-dependencies"),
293        filter_flag: Some("--unused-deps"),
294        mcp_issue_type: None,
295        suppress_token: None,
296        suppress_file_level: false,
297        lsp: true,
298        docs_category: "dependency",
299    },
300    IssueKindMeta {
301        kind: Some(IssueKind::CircularDependency),
302        code: "circular-dependency",
303        aliases: &["circular-dependencies"],
304        label: "Circular Dependencies",
305        config_key: Some("circular-dependencies"),
306        filter_flag: Some("--circular-deps"),
307        mcp_issue_type: Some("circular-deps"),
308        suppress_token: Some("circular-dependency"),
309        suppress_file_level: false,
310        lsp: true,
311        docs_category: "architecture",
312    },
313    IssueKindMeta {
314        kind: Some(IssueKind::ReExportCycle),
315        code: "re-export-cycle",
316        aliases: &["re-export-cycles", "reexport-cycle", "reexport-cycles"],
317        label: "Re-Export Cycles",
318        config_key: Some("re-export-cycle"),
319        filter_flag: Some("--re-export-cycles"),
320        mcp_issue_type: Some("re-export-cycles"),
321        suppress_token: Some("re-export-cycle"),
322        suppress_file_level: true,
323        lsp: true,
324        docs_category: "architecture",
325    },
326    IssueKindMeta {
327        kind: Some(IssueKind::BoundaryViolation),
328        code: "boundary-violation",
329        aliases: &[],
330        label: "Boundary Violations",
331        config_key: Some("boundary-violation"),
332        filter_flag: Some("--boundary-violations"),
333        mcp_issue_type: Some("boundary-violations"),
334        suppress_token: Some("boundary-violation"),
335        suppress_file_level: false,
336        lsp: true,
337        docs_category: "architecture",
338    },
339    IssueKindMeta {
340        kind: None,
341        code: "boundary-coverage",
342        aliases: &["boundary-coverage-violations"],
343        label: "Boundary Coverage",
344        config_key: Some("boundary-violation"),
345        filter_flag: Some("--boundary-violations"),
346        mcp_issue_type: None,
347        suppress_token: Some("boundary-violation"),
348        suppress_file_level: true,
349        lsp: false,
350        docs_category: "architecture",
351    },
352    IssueKindMeta {
353        kind: Some(IssueKind::BoundaryViolation),
354        code: "boundary-call-violation",
355        aliases: &["boundary-call-violations"],
356        label: "Boundary Call Violations",
357        config_key: Some("boundary-violation"),
358        filter_flag: Some("--boundary-violations"),
359        mcp_issue_type: None,
360        suppress_token: Some("boundary-call-violation"),
361        suppress_file_level: false,
362        lsp: false,
363        docs_category: "architecture",
364    },
365    IssueKindMeta {
366        kind: Some(IssueKind::PolicyViolation),
367        code: "policy-violation",
368        aliases: &["policy-violations"],
369        label: "Policy Violations",
370        config_key: Some("policy-violation"),
371        filter_flag: Some("--policy-violations"),
372        mcp_issue_type: Some("policy-violations"),
373        suppress_token: Some("policy-violation"),
374        suppress_file_level: false,
375        lsp: true,
376        docs_category: "architecture",
377    },
378    IssueKindMeta {
379        kind: Some(IssueKind::InvalidClientExport),
380        code: "invalid-client-export",
381        aliases: &["invalid-client-exports"],
382        label: "Invalid Client Exports",
383        config_key: Some("invalid-client-export"),
384        filter_flag: None,
385        mcp_issue_type: None,
386        suppress_token: Some("invalid-client-export"),
387        suppress_file_level: false,
388        lsp: true,
389        docs_category: "framework",
390    },
391    IssueKindMeta {
392        kind: Some(IssueKind::MixedClientServerBarrel),
393        code: "mixed-client-server-barrel",
394        aliases: &["mixed-client-server-barrels"],
395        label: "Mixed Client/Server Barrels",
396        config_key: Some("mixed-client-server-barrel"),
397        filter_flag: None,
398        mcp_issue_type: None,
399        suppress_token: Some("mixed-client-server-barrel"),
400        suppress_file_level: false,
401        lsp: true,
402        docs_category: "framework",
403    },
404    IssueKindMeta {
405        kind: Some(IssueKind::MisplacedDirective),
406        code: "misplaced-directive",
407        aliases: &["misplaced-directives"],
408        label: "Misplaced Directives",
409        config_key: Some("misplaced-directive"),
410        filter_flag: None,
411        mcp_issue_type: None,
412        suppress_token: Some("misplaced-directive"),
413        suppress_file_level: false,
414        lsp: true,
415        docs_category: "framework",
416    },
417    IssueKindMeta {
418        kind: Some(IssueKind::UnprovidedInject),
419        code: "unprovided-inject",
420        aliases: &["unprovided-injects"],
421        label: "Unprovided Injects",
422        config_key: Some("unprovided-injects"),
423        filter_flag: Some("--unprovided-injects"),
424        mcp_issue_type: Some("unprovided-injects"),
425        suppress_token: Some("unprovided-inject"),
426        suppress_file_level: false,
427        lsp: true,
428        docs_category: "framework",
429    },
430    IssueKindMeta {
431        kind: Some(IssueKind::UnrenderedComponent),
432        code: "unrendered-component",
433        aliases: &["unrendered-components"],
434        label: "Unrendered Components",
435        config_key: Some("unrendered-components"),
436        filter_flag: Some("--unrendered-components"),
437        mcp_issue_type: Some("unrendered-components"),
438        suppress_token: Some("unrendered-component"),
439        suppress_file_level: false,
440        lsp: true,
441        docs_category: "framework",
442    },
443    IssueKindMeta {
444        kind: Some(IssueKind::UnusedComponentProp),
445        code: "unused-component-prop",
446        aliases: &["unused-component-props"],
447        label: "Unused Component Props",
448        config_key: Some("unused-component-props"),
449        filter_flag: Some("--unused-component-props"),
450        mcp_issue_type: Some("unused-component-props"),
451        suppress_token: Some("unused-component-prop"),
452        suppress_file_level: false,
453        lsp: true,
454        docs_category: "framework",
455    },
456    IssueKindMeta {
457        kind: Some(IssueKind::UnusedComponentEmit),
458        code: "unused-component-emit",
459        aliases: &["unused-component-emits"],
460        label: "Unused Component Emits",
461        config_key: Some("unused-component-emits"),
462        filter_flag: Some("--unused-component-emits"),
463        mcp_issue_type: Some("unused-component-emits"),
464        suppress_token: Some("unused-component-emit"),
465        suppress_file_level: false,
466        lsp: true,
467        docs_category: "framework",
468    },
469    IssueKindMeta {
470        kind: Some(IssueKind::UnusedComponentInput),
471        code: "unused-component-input",
472        aliases: &["unused-component-inputs"],
473        label: "Unused Component Inputs",
474        config_key: Some("unused-component-inputs"),
475        filter_flag: Some("--unused-component-inputs"),
476        mcp_issue_type: Some("unused-component-inputs"),
477        suppress_token: Some("unused-component-input"),
478        suppress_file_level: false,
479        lsp: true,
480        docs_category: "framework",
481    },
482    IssueKindMeta {
483        kind: Some(IssueKind::UnusedComponentOutput),
484        code: "unused-component-output",
485        aliases: &["unused-component-outputs"],
486        label: "Unused Component Outputs",
487        config_key: Some("unused-component-outputs"),
488        filter_flag: Some("--unused-component-outputs"),
489        mcp_issue_type: Some("unused-component-outputs"),
490        suppress_token: Some("unused-component-output"),
491        suppress_file_level: false,
492        lsp: true,
493        docs_category: "framework",
494    },
495    IssueKindMeta {
496        kind: Some(IssueKind::UnusedSvelteEvent),
497        code: "unused-svelte-event",
498        aliases: &["unused-svelte-events"],
499        label: "Unused Svelte Events",
500        config_key: Some("unused-svelte-events"),
501        filter_flag: Some("--unused-svelte-events"),
502        mcp_issue_type: Some("unused-svelte-events"),
503        suppress_token: Some("unused-svelte-event"),
504        suppress_file_level: false,
505        lsp: true,
506        docs_category: "framework",
507    },
508    IssueKindMeta {
509        kind: Some(IssueKind::UnusedServerAction),
510        code: "unused-server-action",
511        aliases: &["unused-server-actions"],
512        label: "Unused Server Actions",
513        config_key: Some("unused-server-actions"),
514        filter_flag: Some("--unused-server-actions"),
515        mcp_issue_type: Some("unused-server-actions"),
516        suppress_token: Some("unused-server-action"),
517        suppress_file_level: false,
518        lsp: true,
519        docs_category: "framework",
520    },
521    IssueKindMeta {
522        kind: Some(IssueKind::UnusedLoadDataKey),
523        code: "unused-load-data-key",
524        aliases: &["unused-load-data-keys"],
525        label: "Unused Load Data Keys",
526        config_key: Some("unused-load-data-keys"),
527        filter_flag: Some("--unused-load-data-keys"),
528        mcp_issue_type: Some("unused-load-data-keys"),
529        suppress_token: Some("unused-load-data-key"),
530        suppress_file_level: false,
531        lsp: true,
532        docs_category: "framework",
533    },
534    IssueKindMeta {
535        kind: Some(IssueKind::RouteCollision),
536        code: "route-collision",
537        aliases: &["route-collisions"],
538        label: "Route Collisions",
539        config_key: Some("route-collision"),
540        filter_flag: None,
541        mcp_issue_type: None,
542        suppress_token: Some("route-collision"),
543        suppress_file_level: true,
544        lsp: true,
545        docs_category: "framework",
546    },
547    IssueKindMeta {
548        kind: Some(IssueKind::DynamicSegmentNameConflict),
549        code: "dynamic-segment-name-conflict",
550        aliases: &["dynamic-segment-name-conflicts"],
551        label: "Dynamic Segment Conflicts",
552        config_key: Some("dynamic-segment-name-conflict"),
553        filter_flag: None,
554        mcp_issue_type: None,
555        suppress_token: Some("dynamic-segment-name-conflict"),
556        suppress_file_level: true,
557        lsp: true,
558        docs_category: "framework",
559    },
560    IssueKindMeta {
561        kind: Some(IssueKind::StaleSuppression),
562        code: "stale-suppression",
563        aliases: &[],
564        label: "Stale Suppressions",
565        config_key: Some("stale-suppressions"),
566        filter_flag: Some("--stale-suppressions"),
567        mcp_issue_type: Some("stale-suppressions"),
568        suppress_token: None,
569        suppress_file_level: false,
570        lsp: true,
571        docs_category: "source",
572    },
573    IssueKindMeta {
574        kind: Some(IssueKind::PnpmCatalogEntry),
575        code: "unused-catalog-entry",
576        aliases: &["unused-catalog-entries"],
577        label: "Unused Catalog Entries",
578        config_key: Some("unused-catalog-entries"),
579        filter_flag: Some("--unused-catalog-entries"),
580        mcp_issue_type: Some("unused-catalog-entries"),
581        suppress_token: None,
582        suppress_file_level: false,
583        lsp: true,
584        docs_category: "dependency",
585    },
586    IssueKindMeta {
587        kind: Some(IssueKind::EmptyCatalogGroup),
588        code: "empty-catalog-group",
589        aliases: &["empty-catalog-groups"],
590        label: "Empty Catalog Groups",
591        config_key: Some("empty-catalog-groups"),
592        filter_flag: Some("--empty-catalog-groups"),
593        mcp_issue_type: Some("empty-catalog-groups"),
594        suppress_token: None,
595        suppress_file_level: false,
596        lsp: true,
597        docs_category: "dependency",
598    },
599    IssueKindMeta {
600        kind: Some(IssueKind::UnresolvedCatalogReference),
601        code: "unresolved-catalog-reference",
602        aliases: &["unresolved-catalog-references"],
603        label: "Unresolved Catalog References",
604        config_key: Some("unresolved-catalog-references"),
605        filter_flag: Some("--unresolved-catalog-references"),
606        mcp_issue_type: Some("unresolved-catalog-references"),
607        suppress_token: None,
608        suppress_file_level: false,
609        lsp: true,
610        docs_category: "dependency",
611    },
612    IssueKindMeta {
613        kind: Some(IssueKind::UnusedDependencyOverride),
614        code: "unused-dependency-override",
615        aliases: &["unused-dependency-overrides"],
616        label: "Unused Dependency Overrides",
617        config_key: Some("unused-dependency-overrides"),
618        filter_flag: Some("--unused-dependency-overrides"),
619        mcp_issue_type: Some("unused-dependency-overrides"),
620        suppress_token: None,
621        suppress_file_level: false,
622        lsp: true,
623        docs_category: "dependency",
624    },
625    IssueKindMeta {
626        kind: Some(IssueKind::MisconfiguredDependencyOverride),
627        code: "misconfigured-dependency-override",
628        aliases: &["misconfigured-dependency-overrides"],
629        label: "Misconfigured Dependency Overrides",
630        config_key: Some("misconfigured-dependency-overrides"),
631        filter_flag: Some("--misconfigured-dependency-overrides"),
632        mcp_issue_type: Some("misconfigured-dependency-overrides"),
633        suppress_token: None,
634        suppress_file_level: false,
635        lsp: true,
636        docs_category: "dependency",
637    },
638    IssueKindMeta {
639        kind: Some(IssueKind::SecuritySink),
640        code: "security-sink",
641        aliases: &[],
642        label: "Security Sink Candidates",
643        config_key: Some("security-sink"),
644        filter_flag: None,
645        mcp_issue_type: None,
646        suppress_token: Some("security-sink"),
647        suppress_file_level: false,
648        lsp: true,
649        docs_category: "security",
650    },
651    IssueKindMeta {
652        kind: Some(IssueKind::SecurityClientServerLeak),
653        code: "security-client-server-leak",
654        aliases: &[],
655        label: "Security Client-Server Leaks",
656        config_key: Some("security-client-server-leak"),
657        filter_flag: None,
658        mcp_issue_type: None,
659        suppress_token: Some("security-client-server-leak"),
660        suppress_file_level: true,
661        lsp: true,
662        docs_category: "security",
663    },
664    IssueKindMeta {
665        kind: Some(IssueKind::CoverageGaps),
666        code: "coverage-gaps",
667        aliases: &[],
668        label: "Coverage Gaps",
669        config_key: Some("coverage-gaps"),
670        filter_flag: None,
671        mcp_issue_type: None,
672        suppress_token: Some("coverage-gaps"),
673        suppress_file_level: true,
674        lsp: false,
675        docs_category: "health",
676    },
677    IssueKindMeta {
678        kind: Some(IssueKind::FeatureFlag),
679        code: "feature-flag",
680        aliases: &[],
681        label: "Feature Flags",
682        config_key: Some("feature-flags"),
683        filter_flag: None,
684        mcp_issue_type: None,
685        suppress_token: Some("feature-flag"),
686        suppress_file_level: false,
687        lsp: false,
688        docs_category: "flags",
689    },
690    IssueKindMeta {
691        kind: Some(IssueKind::Complexity),
692        code: "complexity",
693        aliases: &[],
694        label: "Complexity",
695        config_key: None,
696        filter_flag: None,
697        mcp_issue_type: None,
698        suppress_token: Some("complexity"),
699        suppress_file_level: false,
700        lsp: false,
701        docs_category: "health",
702    },
703    IssueKindMeta {
704        kind: Some(IssueKind::PropDrilling),
705        code: "prop-drilling",
706        aliases: &[],
707        label: "Prop Drilling",
708        config_key: Some("prop-drilling"),
709        filter_flag: None,
710        mcp_issue_type: None,
711        suppress_token: Some("prop-drilling"),
712        suppress_file_level: false,
713        lsp: false,
714        docs_category: "source",
715    },
716    IssueKindMeta {
717        kind: Some(IssueKind::ThinWrapper),
718        code: "thin-wrapper",
719        aliases: &["thin-wrappers"],
720        label: "Thin Wrappers",
721        config_key: Some("thin-wrapper"),
722        filter_flag: None,
723        mcp_issue_type: None,
724        suppress_token: Some("thin-wrapper"),
725        suppress_file_level: false,
726        lsp: false,
727        docs_category: "source",
728    },
729    IssueKindMeta {
730        kind: Some(IssueKind::DuplicatePropShape),
731        code: "duplicate-prop-shape",
732        aliases: &["duplicate-prop-shapes"],
733        label: "Duplicate Prop Shapes",
734        config_key: Some("duplicate-prop-shape"),
735        filter_flag: None,
736        mcp_issue_type: None,
737        suppress_token: Some("duplicate-prop-shape"),
738        suppress_file_level: false,
739        lsp: false,
740        docs_category: "source",
741    },
742];
743
744/// Shared contract facts for serialized `AnalysisResults` arrays.
745#[derive(Debug, Clone, Copy, PartialEq, Eq)]
746#[non_exhaustive]
747pub struct IssueResultMeta {
748    /// Canonical issue code that owns this result array.
749    pub code: &'static str,
750    /// Explanation emitted in dead-code `_meta.rules`.
751    pub meta_description: &'static str,
752    /// Documentation path emitted in dead-code `_meta.rules`.
753    pub meta_docs_path: &'static str,
754    /// Human-readable name emitted in dead-code `_meta.rules`.
755    pub meta_name: &'static str,
756    /// Label used by CI summary tables.
757    pub summary_label: &'static str,
758    /// Documentation anchor under `/explanations/dead-code`.
759    pub docs_anchor: &'static str,
760    /// Serialized `AnalysisResults` array key that carries this issue row.
761    pub result_key: &'static str,
762    /// Whether `result_key` contributes to `AnalysisResults::total_issues()`.
763    pub counts_in_total: bool,
764}
765
766/// TypeScript backwards-compat alias emitted for a dead-code result row.
767#[derive(Debug, Clone, Copy, PartialEq, Eq)]
768pub struct TsAliasMeta {
769    /// Bare alias name kept available from the published `fallow/types` subpath.
770    pub name: &'static str,
771    /// Generated `*Finding` wrapper type the alias resolves to.
772    pub parent: &'static str,
773}
774
775/// All shared issue-to-result metadata rows.
776pub const ISSUE_RESULT_META: &[IssueResultMeta] = &[
777    IssueResultMeta {
778        code: "unused-file",
779        meta_description: "Source files that are not imported by any other module and are not entry points. Detection uses graph reachability from configured entry points.",
780        meta_docs_path: "explanations/dead-code#unused-files",
781        meta_name: "Unused Files",
782        summary_label: "Unused files",
783        docs_anchor: "unused-files",
784        result_key: "unused_files",
785        counts_in_total: true,
786    },
787    IssueResultMeta {
788        code: "unused-export",
789        meta_description: "Named exports that are never imported by any other module in the project, including direct exports and re-exports through barrel files.",
790        meta_docs_path: "explanations/dead-code#unused-exports",
791        meta_name: "Unused Exports",
792        summary_label: "Unused exports",
793        docs_anchor: "unused-exports",
794        result_key: "unused_exports",
795        counts_in_total: true,
796    },
797    IssueResultMeta {
798        code: "unused-type",
799        meta_description: "Type-only exports that are never imported. These do not generate runtime code but add maintenance burden.",
800        meta_docs_path: "explanations/dead-code#unused-types",
801        meta_name: "Unused Type Exports",
802        summary_label: "Unused types",
803        docs_anchor: "unused-types",
804        result_key: "unused_types",
805        counts_in_total: true,
806    },
807    IssueResultMeta {
808        code: "private-type-leak",
809        meta_description: "Exported values or types whose public TypeScript signature references a same-file type declaration that is not exported.",
810        meta_docs_path: "explanations/dead-code#private-type-leaks",
811        meta_name: "Private Type Leaks",
812        summary_label: "Private type leaks",
813        docs_anchor: "private-type-leaks",
814        result_key: "private_type_leaks",
815        counts_in_total: true,
816    },
817    IssueResultMeta {
818        code: "unused-dependency",
819        meta_description: "Packages listed in dependencies that are never imported or required by any source file.",
820        meta_docs_path: "explanations/dead-code#unused-dependencies",
821        meta_name: "Unused Dependencies",
822        summary_label: "Unused dependencies",
823        docs_anchor: "unused-dependencies",
824        result_key: "unused_dependencies",
825        counts_in_total: true,
826    },
827    IssueResultMeta {
828        code: "unused-dev-dependency",
829        meta_description: "Packages listed in devDependencies that are never imported by test files, config files, or scripts.",
830        meta_docs_path: "explanations/dead-code#unused-devdependencies",
831        meta_name: "Unused Dev Dependencies",
832        summary_label: "Unused devDependencies",
833        docs_anchor: "unused-dependencies",
834        result_key: "unused_dev_dependencies",
835        counts_in_total: true,
836    },
837    IssueResultMeta {
838        code: "unused-optional-dependency",
839        meta_description: "Packages listed in optionalDependencies that are never imported.",
840        meta_docs_path: "explanations/dead-code#unused-optionaldependencies",
841        meta_name: "Unused Optional Dependencies",
842        summary_label: "Unused optionalDependencies",
843        docs_anchor: "unused-dependencies",
844        result_key: "unused_optional_dependencies",
845        counts_in_total: true,
846    },
847    IssueResultMeta {
848        code: "unused-enum-member",
849        meta_description: "Enum members that are never referenced in the codebase.",
850        meta_docs_path: "explanations/dead-code#unused-enum-members",
851        meta_name: "Unused Enum Members",
852        summary_label: "Unused enum members",
853        docs_anchor: "unused-enum-members",
854        result_key: "unused_enum_members",
855        counts_in_total: true,
856    },
857    IssueResultMeta {
858        code: "unused-class-member",
859        meta_description: "Class methods and properties that are never referenced outside the class.",
860        meta_docs_path: "explanations/dead-code#unused-class-members",
861        meta_name: "Unused Class Members",
862        summary_label: "Unused class members",
863        docs_anchor: "unused-class-members",
864        result_key: "unused_class_members",
865        counts_in_total: true,
866    },
867    IssueResultMeta {
868        code: "unused-store-member",
869        meta_description: "Pinia store members declared but never accessed by any consumer project-wide.",
870        meta_docs_path: "explanations/dead-code#unused-store-members",
871        meta_name: "Unused Store Members",
872        summary_label: "Unused store members",
873        docs_anchor: "unused-store-members",
874        result_key: "unused_store_members",
875        counts_in_total: true,
876    },
877    IssueResultMeta {
878        code: "unresolved-import",
879        meta_description: "Import specifiers that could not be resolved to a file on disk.",
880        meta_docs_path: "explanations/dead-code#unresolved-imports",
881        meta_name: "Unresolved Imports",
882        summary_label: "Unresolved imports",
883        docs_anchor: "unresolved-imports",
884        result_key: "unresolved_imports",
885        counts_in_total: true,
886    },
887    IssueResultMeta {
888        code: "unlisted-dependency",
889        meta_description: "Packages imported in source code but not listed in package.json.",
890        meta_docs_path: "explanations/dead-code#unlisted-dependencies",
891        meta_name: "Unlisted Dependencies",
892        summary_label: "Unlisted dependencies",
893        docs_anchor: "unlisted-dependencies",
894        result_key: "unlisted_dependencies",
895        counts_in_total: true,
896    },
897    IssueResultMeta {
898        code: "duplicate-export",
899        meta_description: "The same export name is defined in multiple modules.",
900        meta_docs_path: "explanations/dead-code#duplicate-exports",
901        meta_name: "Duplicate Exports",
902        summary_label: "Duplicate exports",
903        docs_anchor: "duplicate-exports",
904        result_key: "duplicate_exports",
905        counts_in_total: true,
906    },
907    IssueResultMeta {
908        code: "type-only-dependency",
909        meta_description: "Production dependencies that are only imported via type-only imports.",
910        meta_docs_path: "explanations/dead-code#type-only-dependencies",
911        meta_name: "Type-only Dependencies",
912        summary_label: "Type-only dependencies",
913        docs_anchor: "type-only-dependencies",
914        result_key: "type_only_dependencies",
915        counts_in_total: true,
916    },
917    IssueResultMeta {
918        code: "test-only-dependency",
919        meta_description: "Production dependencies that are only imported from test files.",
920        meta_docs_path: "explanations/dead-code#test-only-dependencies",
921        meta_name: "Test-only Dependencies",
922        summary_label: "Test-only dependencies",
923        docs_anchor: "test-only-dependencies",
924        result_key: "test_only_dependencies",
925        counts_in_total: true,
926    },
927    IssueResultMeta {
928        code: "circular-dependency",
929        meta_description: "A cycle in the module import graph.",
930        meta_docs_path: "explanations/dead-code#circular-dependencies",
931        meta_name: "Circular Dependencies",
932        summary_label: "Circular dependencies",
933        docs_anchor: "circular-dependencies",
934        result_key: "circular_dependencies",
935        counts_in_total: true,
936    },
937    IssueResultMeta {
938        code: "re-export-cycle",
939        meta_description: "A barrel file re-exports from another barrel that ultimately re-exports back.",
940        meta_docs_path: "explanations/dead-code#re-export-cycles",
941        meta_name: "Re-Export Cycles",
942        summary_label: "Re-export cycles",
943        docs_anchor: "re-export-cycles",
944        result_key: "re_export_cycles",
945        counts_in_total: true,
946    },
947    IssueResultMeta {
948        code: "boundary-violation",
949        meta_description: "A module imports from a zone that its configured boundary rules do not allow.",
950        meta_docs_path: "explanations/dead-code#boundary-violations",
951        meta_name: "Boundary Violations",
952        summary_label: "Boundary violations",
953        docs_anchor: "boundary-violations",
954        result_key: "boundary_violations",
955        counts_in_total: true,
956    },
957    IssueResultMeta {
958        code: "boundary-coverage",
959        meta_description: "A reachable source file is not assigned to any configured boundary zone while boundary coverage is required.",
960        meta_docs_path: "explanations/dead-code#boundary-violations",
961        meta_name: "Boundary Coverage",
962        summary_label: "Boundary coverage",
963        docs_anchor: "boundary-violations",
964        result_key: "boundary_coverage_violations",
965        counts_in_total: true,
966    },
967    IssueResultMeta {
968        code: "boundary-call-violation",
969        meta_description: "A file classified into a boundary zone calls a callee matching one of the zone's forbidden call patterns.",
970        meta_docs_path: "explanations/dead-code#boundary-violations",
971        meta_name: "Boundary Call Violation",
972        summary_label: "Boundary calls",
973        docs_anchor: "boundary-violations",
974        result_key: "boundary_call_violations",
975        counts_in_total: true,
976    },
977    IssueResultMeta {
978        code: "policy-violation",
979        meta_description: "A call site, import, or catalogue-derived effect matched a configured rule pack rule.",
980        meta_docs_path: "explanations/dead-code#policy-violations",
981        meta_name: "Policy Violation",
982        summary_label: "Policy violations",
983        docs_anchor: "policy-violations",
984        result_key: "policy_violations",
985        counts_in_total: true,
986    },
987    IssueResultMeta {
988        code: "invalid-client-export",
989        meta_description: "A file carrying the use client directive also exports a Next.js server-only or route-segment config name.",
990        meta_docs_path: "explanations/dead-code#invalid-client-exports",
991        meta_name: "Invalid client export",
992        summary_label: "Invalid client exports",
993        docs_anchor: "invalid-client-exports",
994        result_key: "invalid_client_exports",
995        counts_in_total: true,
996    },
997    IssueResultMeta {
998        code: "mixed-client-server-barrel",
999        meta_description: "A barrel file forwards a name from a use client module alongside a name from a server-only module.",
1000        meta_docs_path: "explanations/dead-code#mixed-client-server-barrels",
1001        meta_name: "Mixed client/server barrel",
1002        summary_label: "Mixed client/server barrels",
1003        docs_anchor: "mixed-client-server-barrels",
1004        result_key: "mixed_client_server_barrels",
1005        counts_in_total: true,
1006    },
1007    IssueResultMeta {
1008        code: "misplaced-directive",
1009        meta_description: "A use client or use server directive string appears after a non-directive statement and is ignored.",
1010        meta_docs_path: "explanations/dead-code#misplaced-directives",
1011        meta_name: "Misplaced directive",
1012        summary_label: "Misplaced directives",
1013        docs_anchor: "misplaced-directives",
1014        result_key: "misplaced_directives",
1015        counts_in_total: true,
1016    },
1017    IssueResultMeta {
1018        code: "unprovided-inject",
1019        meta_description: "A Vue inject or Svelte getContext reads a dependency-injection key that no matching provider supplies.",
1020        meta_docs_path: "explanations/dead-code#unprovided-injects",
1021        meta_name: "Unprovided injects",
1022        summary_label: "Unprovided injects",
1023        docs_anchor: "unprovided-inject",
1024        result_key: "unprovided_injects",
1025        counts_in_total: true,
1026    },
1027    IssueResultMeta {
1028        code: "unrendered-component",
1029        meta_description: "A Vue or Svelte single-file component is reachable through the graph but rendered nowhere in the project.",
1030        meta_docs_path: "explanations/dead-code#unrendered-components",
1031        meta_name: "Unrendered components",
1032        summary_label: "Unrendered components",
1033        docs_anchor: "unrendered-component",
1034        result_key: "unrendered_components",
1035        counts_in_total: true,
1036    },
1037    IssueResultMeta {
1038        code: "unused-component-prop",
1039        meta_description: "A declared Vue, Svelte, React, or Preact component prop is referenced nowhere inside its own component.",
1040        meta_docs_path: "explanations/dead-code#unused-component-props",
1041        meta_name: "Unused component props",
1042        summary_label: "Unused component props",
1043        docs_anchor: "unused-component-prop",
1044        result_key: "unused_component_props",
1045        counts_in_total: true,
1046    },
1047    IssueResultMeta {
1048        code: "unused-component-emit",
1049        meta_description: "A Vue script setup defineEmits event is emitted nowhere in its own component.",
1050        meta_docs_path: "explanations/dead-code#unused-component-emits",
1051        meta_name: "Unused component emits",
1052        summary_label: "Unused component emits",
1053        docs_anchor: "unused-component-emit",
1054        result_key: "unused_component_emits",
1055        counts_in_total: true,
1056    },
1057    IssueResultMeta {
1058        code: "unused-component-input",
1059        meta_description: "An Angular input is read nowhere in its own component.",
1060        meta_docs_path: "explanations/dead-code#unused-component-inputs",
1061        meta_name: "Unused component inputs",
1062        summary_label: "Unused component inputs",
1063        docs_anchor: "unused-component-input",
1064        result_key: "unused_component_inputs",
1065        counts_in_total: true,
1066    },
1067    IssueResultMeta {
1068        code: "unused-component-output",
1069        meta_description: "An Angular output is emitted nowhere in its own component.",
1070        meta_docs_path: "explanations/dead-code#unused-component-outputs",
1071        meta_name: "Unused component outputs",
1072        summary_label: "Unused component outputs",
1073        docs_anchor: "unused-component-output",
1074        result_key: "unused_component_outputs",
1075        counts_in_total: true,
1076    },
1077    IssueResultMeta {
1078        code: "unused-svelte-event",
1079        meta_description: "A Svelte component dispatches a custom event whose name is listened to nowhere in the analyzed project.",
1080        meta_docs_path: "explanations/dead-code#unused-svelte-events",
1081        meta_name: "Unused Svelte events",
1082        summary_label: "Unused Svelte events",
1083        docs_anchor: "unused-svelte-event",
1084        result_key: "unused_svelte_events",
1085        counts_in_total: true,
1086    },
1087    IssueResultMeta {
1088        code: "unused-server-action",
1089        meta_description: "A Next.js Server Action exported from a use server file is referenced by no code in the project.",
1090        meta_docs_path: "explanations/dead-code#unused-server-actions",
1091        meta_name: "Unused server actions",
1092        summary_label: "Unused server actions",
1093        docs_anchor: "unused-server-action",
1094        result_key: "unused_server_actions",
1095        counts_in_total: true,
1096    },
1097    IssueResultMeta {
1098        code: "unused-load-data-key",
1099        meta_description: "A SvelteKit load return-object key is read by no route or project-wide consumer.",
1100        meta_docs_path: "explanations/dead-code#unused-load-data-keys",
1101        meta_name: "Unused load data keys",
1102        summary_label: "Unused load data keys",
1103        docs_anchor: "unused-load-data-key",
1104        result_key: "unused_load_data_keys",
1105        counts_in_total: true,
1106    },
1107    IssueResultMeta {
1108        code: "route-collision",
1109        meta_description: "Two or more Next.js App Router route files resolve to the same URL within one app root.",
1110        meta_docs_path: "explanations/dead-code#route-collisions",
1111        meta_name: "Route collision",
1112        summary_label: "Route collisions",
1113        docs_anchor: "route-collisions",
1114        result_key: "route_collisions",
1115        counts_in_total: true,
1116    },
1117    IssueResultMeta {
1118        code: "dynamic-segment-name-conflict",
1119        meta_description: "Sibling Next.js dynamic route segments use different slug names at the same position.",
1120        meta_docs_path: "explanations/dead-code#dynamic-segment-name-conflicts",
1121        meta_name: "Dynamic segment name conflict",
1122        summary_label: "Dynamic segment conflicts",
1123        docs_anchor: "dynamic-segment-name-conflicts",
1124        result_key: "dynamic_segment_name_conflicts",
1125        counts_in_total: true,
1126    },
1127    IssueResultMeta {
1128        code: "stale-suppression",
1129        meta_description: "A fallow suppression comment or tag no longer matches any active issue.",
1130        meta_docs_path: "explanations/dead-code#stale-suppressions",
1131        meta_name: "Stale Suppressions",
1132        summary_label: "Stale suppressions",
1133        docs_anchor: "stale-suppressions",
1134        result_key: "stale_suppressions",
1135        counts_in_total: true,
1136    },
1137    IssueResultMeta {
1138        code: "unused-catalog-entry",
1139        meta_description: "A package manager catalog entry is not referenced by any workspace package.json.",
1140        meta_docs_path: "explanations/dead-code#unused-catalog-entries",
1141        meta_name: "Unused catalog entry",
1142        summary_label: "Unused catalog entries",
1143        docs_anchor: "unused-catalog-entries",
1144        result_key: "unused_catalog_entries",
1145        counts_in_total: true,
1146    },
1147    IssueResultMeta {
1148        code: "empty-catalog-group",
1149        meta_description: "A named package manager catalog group has no package entries.",
1150        meta_docs_path: "explanations/dead-code#empty-catalog-groups",
1151        meta_name: "Empty catalog group",
1152        summary_label: "Empty catalog groups",
1153        docs_anchor: "empty-catalog-groups",
1154        result_key: "empty_catalog_groups",
1155        counts_in_total: true,
1156    },
1157    IssueResultMeta {
1158        code: "unresolved-catalog-reference",
1159        meta_description: "A workspace package.json uses a catalog protocol reference that no catalog declares.",
1160        meta_docs_path: "explanations/dead-code#unresolved-catalog-references",
1161        meta_name: "Unresolved catalog reference",
1162        summary_label: "Unresolved catalog references",
1163        docs_anchor: "unresolved-catalog-references",
1164        result_key: "unresolved_catalog_references",
1165        counts_in_total: true,
1166    },
1167    IssueResultMeta {
1168        code: "unused-dependency-override",
1169        meta_description: "A pnpm dependency override targets a package not declared by any workspace package and not present in the lockfile.",
1170        meta_docs_path: "explanations/dead-code#unused-dependency-overrides",
1171        meta_name: "Unused pnpm dependency override",
1172        summary_label: "Unused dependency overrides",
1173        docs_anchor: "unused-dependency-overrides",
1174        result_key: "unused_dependency_overrides",
1175        counts_in_total: true,
1176    },
1177    IssueResultMeta {
1178        code: "misconfigured-dependency-override",
1179        meta_description: "A pnpm dependency override key or value does not parse as a valid override spec.",
1180        meta_docs_path: "explanations/dead-code#misconfigured-dependency-overrides",
1181        meta_name: "Misconfigured pnpm dependency override",
1182        summary_label: "Misconfigured dependency overrides",
1183        docs_anchor: "misconfigured-dependency-overrides",
1184        result_key: "misconfigured_dependency_overrides",
1185        counts_in_total: true,
1186    },
1187    IssueResultMeta {
1188        code: "prop-drilling",
1189        meta_description: "A React or Preact prop is forwarded unchanged through multiple pass-through components to a distant consumer.",
1190        meta_docs_path: "explanations/dead-code#prop-drilling",
1191        meta_name: "Prop drilling",
1192        summary_label: "Prop drilling",
1193        docs_anchor: "prop-drilling",
1194        result_key: "prop_drilling_chains",
1195        counts_in_total: false,
1196    },
1197    IssueResultMeta {
1198        code: "thin-wrapper",
1199        meta_description: "A React or Preact component is structural indirection around a single spread-forwarded child render.",
1200        meta_docs_path: "explanations/dead-code#thin-wrapper",
1201        meta_name: "Thin wrapper",
1202        summary_label: "Thin wrappers",
1203        docs_anchor: "thin-wrapper",
1204        result_key: "thin_wrappers",
1205        counts_in_total: false,
1206    },
1207    IssueResultMeta {
1208        code: "duplicate-prop-shape",
1209        meta_description: "Multiple React or Preact components declare an identical significant prop-name set.",
1210        meta_docs_path: "explanations/dead-code#duplicate-prop-shape",
1211        meta_name: "Duplicate prop shape",
1212        summary_label: "Duplicate prop shapes",
1213        docs_anchor: "duplicate-prop-shape",
1214        result_key: "duplicate_prop_shapes",
1215        counts_in_total: false,
1216    },
1217];
1218
1219/// Canonical names and aliases accepted by `IssueKind::parse`.
1220pub const KNOWN_ISSUE_KIND_NAMES: &[&str] = &[
1221    "unused-file",
1222    "unused-export",
1223    "unused-type",
1224    "private-type-leak",
1225    "unused-dependency",
1226    "unused-dev-dependency",
1227    "unused-enum-member",
1228    "unused-class-member",
1229    "unresolved-import",
1230    "unlisted-dependency",
1231    "duplicate-export",
1232    "code-duplication",
1233    "circular-dependency",
1234    "circular-dependencies",
1235    "re-export-cycle",
1236    "re-export-cycles",
1237    "reexport-cycle",
1238    "reexport-cycles",
1239    "type-only-dependency",
1240    "test-only-dependency",
1241    "boundary-violation",
1242    "boundary-call-violation",
1243    "boundary-call-violations",
1244    "coverage-gaps",
1245    "feature-flag",
1246    "complexity",
1247    "stale-suppression",
1248    "unused-catalog-entry",
1249    "unused-catalog-entries",
1250    "empty-catalog-group",
1251    "empty-catalog-groups",
1252    "unresolved-catalog-reference",
1253    "unresolved-catalog-references",
1254    "unused-dependency-override",
1255    "unused-dependency-overrides",
1256    "misconfigured-dependency-override",
1257    "misconfigured-dependency-overrides",
1258    "security-client-server-leak",
1259    "security-sink",
1260    "policy-violation",
1261    "policy-violations",
1262    "invalid-client-export",
1263    "invalid-client-exports",
1264    "mixed-client-server-barrel",
1265    "mixed-client-server-barrels",
1266    "misplaced-directive",
1267    "misplaced-directives",
1268    "unused-store-member",
1269    "unused-store-members",
1270    "unprovided-inject",
1271    "unprovided-injects",
1272    "route-collision",
1273    "route-collisions",
1274    "dynamic-segment-name-conflict",
1275    "dynamic-segment-name-conflicts",
1276    "unrendered-component",
1277    "unrendered-components",
1278    "unused-component-prop",
1279    "unused-component-props",
1280    "unused-component-emit",
1281    "unused-component-emits",
1282    "unused-component-input",
1283    "unused-component-inputs",
1284    "unused-component-output",
1285    "unused-component-outputs",
1286    "unused-server-action",
1287    "unused-server-actions",
1288    "unused-load-data-key",
1289    "unused-load-data-keys",
1290    "prop-drilling",
1291    "thin-wrapper",
1292    "thin-wrappers",
1293    "duplicate-prop-shape",
1294    "duplicate-prop-shapes",
1295    "unused-svelte-event",
1296    "unused-svelte-events",
1297];
1298
1299/// CLI filter flags on `fallow dead-code` that scope output to one issue family.
1300pub const DEAD_CODE_FILTER_FLAGS: &[&str] = &[
1301    "--unused-files",
1302    "--unused-exports",
1303    "--unused-types",
1304    "--private-type-leaks",
1305    "--unused-deps",
1306    "--unused-enum-members",
1307    "--unused-class-members",
1308    "--unused-store-members",
1309    "--unprovided-injects",
1310    "--unrendered-components",
1311    "--unused-component-props",
1312    "--unused-component-emits",
1313    "--unused-component-inputs",
1314    "--unused-component-outputs",
1315    "--unused-svelte-events",
1316    "--unused-server-actions",
1317    "--unused-load-data-keys",
1318    "--unresolved-imports",
1319    "--unlisted-deps",
1320    "--duplicate-exports",
1321    "--circular-deps",
1322    "--re-export-cycles",
1323    "--boundary-violations",
1324    "--policy-violations",
1325    "--stale-suppressions",
1326    "--unused-catalog-entries",
1327    "--empty-catalog-groups",
1328    "--unresolved-catalog-references",
1329    "--unused-dependency-overrides",
1330    "--misconfigured-dependency-overrides",
1331];
1332
1333/// MCP issue selector names mapped to dead-code CLI flags.
1334pub const MCP_ISSUE_TYPE_FLAGS: &[(&str, &str)] = &[
1335    ("unused-files", "--unused-files"),
1336    ("unused-exports", "--unused-exports"),
1337    ("unused-types", "--unused-types"),
1338    ("private-type-leaks", "--private-type-leaks"),
1339    ("unused-deps", "--unused-deps"),
1340    ("unused-enum-members", "--unused-enum-members"),
1341    ("unused-class-members", "--unused-class-members"),
1342    ("unused-store-members", "--unused-store-members"),
1343    ("unprovided-injects", "--unprovided-injects"),
1344    ("unrendered-components", "--unrendered-components"),
1345    ("unused-component-props", "--unused-component-props"),
1346    ("unused-component-emits", "--unused-component-emits"),
1347    ("unused-component-inputs", "--unused-component-inputs"),
1348    ("unused-component-outputs", "--unused-component-outputs"),
1349    ("unused-svelte-events", "--unused-svelte-events"),
1350    ("unused-server-actions", "--unused-server-actions"),
1351    ("unused-load-data-keys", "--unused-load-data-keys"),
1352    ("unresolved-imports", "--unresolved-imports"),
1353    ("unlisted-deps", "--unlisted-deps"),
1354    ("duplicate-exports", "--duplicate-exports"),
1355    ("circular-deps", "--circular-deps"),
1356    ("re-export-cycles", "--re-export-cycles"),
1357    ("boundary-violations", "--boundary-violations"),
1358    ("policy-violations", "--policy-violations"),
1359    ("stale-suppressions", "--stale-suppressions"),
1360    ("unused-catalog-entries", "--unused-catalog-entries"),
1361    ("empty-catalog-groups", "--empty-catalog-groups"),
1362    (
1363        "unresolved-catalog-references",
1364        "--unresolved-catalog-references",
1365    ),
1366    (
1367        "unused-dependency-overrides",
1368        "--unused-dependency-overrides",
1369    ),
1370    (
1371        "misconfigured-dependency-overrides",
1372        "--misconfigured-dependency-overrides",
1373    ),
1374];
1375
1376/// Result issue codes emitted by the dead-code CodeClimate formatter.
1377pub const CODECLIMATE_RESULT_CODES: &[&str] = &[
1378    "unused-file",
1379    "unused-export",
1380    "unused-type",
1381    "private-type-leak",
1382    "unused-dependency",
1383    "unused-dev-dependency",
1384    "unused-optional-dependency",
1385    "unused-enum-member",
1386    "unused-class-member",
1387    "unused-store-member",
1388    "unresolved-import",
1389    "unlisted-dependency",
1390    "duplicate-export",
1391    "type-only-dependency",
1392    "test-only-dependency",
1393    "circular-dependency",
1394    "re-export-cycle",
1395    "boundary-violation",
1396    "boundary-coverage",
1397    "boundary-call-violation",
1398    "policy-violation",
1399    "invalid-client-export",
1400    "mixed-client-server-barrel",
1401    "misplaced-directive",
1402    "unprovided-inject",
1403    "unrendered-component",
1404    "unused-component-prop",
1405    "unused-component-emit",
1406    "unused-component-input",
1407    "unused-component-output",
1408    "unused-svelte-event",
1409    "unused-server-action",
1410    "unused-load-data-key",
1411    "route-collision",
1412    "dynamic-segment-name-conflict",
1413    "stale-suppression",
1414    "unused-catalog-entry",
1415    "empty-catalog-group",
1416    "unresolved-catalog-reference",
1417    "unused-dependency-override",
1418    "misconfigured-dependency-override",
1419];
1420
1421/// Lookup metadata by canonical code.
1422#[must_use]
1423pub fn issue_meta_by_code(code: &str) -> Option<&'static IssueKindMeta> {
1424    ISSUE_KIND_META.iter().find(|meta| meta.code == code)
1425}
1426
1427/// Lookup metadata by canonical code or alias.
1428#[must_use]
1429pub fn issue_meta_for_token(token: &str) -> Option<&'static IssueKindMeta> {
1430    ISSUE_KIND_META
1431        .iter()
1432        .find(|meta| meta.code == token || meta.aliases.contains(&token))
1433}
1434
1435/// Lookup metadata by backing issue kind.
1436#[must_use]
1437pub fn issue_meta_by_kind(kind: IssueKind) -> Option<&'static IssueKindMeta> {
1438    ISSUE_KIND_META.iter().find(|meta| meta.kind == Some(kind))
1439}
1440
1441/// Lookup serialized result metadata by canonical issue code.
1442#[must_use]
1443pub fn issue_result_meta_by_code(code: &str) -> Option<&'static IssueResultMeta> {
1444    ISSUE_RESULT_META.iter().find(|meta| meta.code == code)
1445}
1446
1447/// SARIF rule ids used by CI formatters for a canonical issue code.
1448#[must_use]
1449pub fn issue_sarif_rule_ids(code: &str) -> Vec<String> {
1450    let mut ids = vec![format!("fallow/{code}")];
1451    if code == "stale-suppression" {
1452        ids.push("fallow/missing-suppression-reason".to_string());
1453    }
1454    ids
1455}
1456
1457/// CodeClimate check names used by CI formatters for a canonical issue code.
1458#[must_use]
1459pub fn issue_codeclimate_check_names(code: &str) -> Vec<String> {
1460    if !CODECLIMATE_RESULT_CODES.contains(&code) {
1461        return Vec::new();
1462    }
1463    issue_sarif_rule_ids(code)
1464}
1465
1466/// Documentation anchor under `/explanations/dead-code` for a canonical issue
1467/// code.
1468#[must_use]
1469pub fn issue_docs_anchor(code: &str) -> Option<&'static str> {
1470    issue_result_meta_by_code(code).map(|meta| meta.docs_anchor)
1471}
1472
1473/// Published TypeScript alias policy for backwards-compatible bare names.
1474#[must_use]
1475pub fn issue_ts_alias(code: &str) -> Option<TsAliasMeta> {
1476    let alias = match code {
1477        "unused-file" => TsAliasMeta {
1478            name: "UnusedFile",
1479            parent: "UnusedFileFinding",
1480        },
1481        "unused-export" => TsAliasMeta {
1482            name: "UnusedExport",
1483            parent: "UnusedExportFinding",
1484        },
1485        "private-type-leak" => TsAliasMeta {
1486            name: "PrivateTypeLeak",
1487            parent: "PrivateTypeLeakFinding",
1488        },
1489        "unused-dependency" => TsAliasMeta {
1490            name: "UnusedDependency",
1491            parent: "UnusedDependencyFinding",
1492        },
1493        "unused-dev-dependency" => TsAliasMeta {
1494            name: "UnusedDependency",
1495            parent: "UnusedDevDependencyFinding",
1496        },
1497        "unused-optional-dependency" => TsAliasMeta {
1498            name: "UnusedDependency",
1499            parent: "UnusedOptionalDependencyFinding",
1500        },
1501        "unused-enum-member" => TsAliasMeta {
1502            name: "UnusedMember",
1503            parent: "UnusedEnumMemberFinding",
1504        },
1505        "unused-class-member" => TsAliasMeta {
1506            name: "UnusedMember",
1507            parent: "UnusedClassMemberFinding",
1508        },
1509        "unused-store-member" => TsAliasMeta {
1510            name: "UnusedMember",
1511            parent: "UnusedStoreMemberFinding",
1512        },
1513        "unresolved-import" => TsAliasMeta {
1514            name: "UnresolvedImport",
1515            parent: "UnresolvedImportFinding",
1516        },
1517        "unlisted-dependency" => TsAliasMeta {
1518            name: "UnlistedDependency",
1519            parent: "UnlistedDependencyFinding",
1520        },
1521        "duplicate-export" => TsAliasMeta {
1522            name: "DuplicateExport",
1523            parent: "DuplicateExportFinding",
1524        },
1525        "type-only-dependency" => TsAliasMeta {
1526            name: "TypeOnlyDependency",
1527            parent: "TypeOnlyDependencyFinding",
1528        },
1529        "test-only-dependency" => TsAliasMeta {
1530            name: "TestOnlyDependency",
1531            parent: "TestOnlyDependencyFinding",
1532        },
1533        "circular-dependency" => TsAliasMeta {
1534            name: "CircularDependency",
1535            parent: "CircularDependencyFinding",
1536        },
1537        "re-export-cycle" => TsAliasMeta {
1538            name: "ReExportCycle",
1539            parent: "ReExportCycleFinding",
1540        },
1541        "boundary-violation" => TsAliasMeta {
1542            name: "BoundaryViolation",
1543            parent: "BoundaryViolationFinding",
1544        },
1545        "unused-catalog-entry" => TsAliasMeta {
1546            name: "UnusedCatalogEntry",
1547            parent: "UnusedCatalogEntryFinding",
1548        },
1549        "empty-catalog-group" => TsAliasMeta {
1550            name: "EmptyCatalogGroup",
1551            parent: "EmptyCatalogGroupFinding",
1552        },
1553        "unresolved-catalog-reference" => TsAliasMeta {
1554            name: "UnresolvedCatalogReference",
1555            parent: "UnresolvedCatalogReferenceFinding",
1556        },
1557        "unused-dependency-override" => TsAliasMeta {
1558            name: "UnusedDependencyOverride",
1559            parent: "UnusedDependencyOverrideFinding",
1560        },
1561        "misconfigured-dependency-override" => TsAliasMeta {
1562            name: "MisconfiguredDependencyOverride",
1563            parent: "MisconfiguredDependencyOverrideFinding",
1564        },
1565        _ => return None,
1566    };
1567    Some(alias)
1568}
1569
1570/// Rows exposed by the LSP issue-type capability.
1571pub fn diagnostic_issue_metas() -> impl Iterator<Item = &'static IssueKindMeta> {
1572    ISSUE_KIND_META.iter().filter(|meta| meta.lsp)
1573}
1574
1575/// Rows that map to a serialized `AnalysisResults` array.
1576pub fn result_issue_metas() -> impl Iterator<Item = &'static IssueResultMeta> {
1577    ISSUE_RESULT_META.iter()
1578}
1579
1580/// Rows whose serialized `AnalysisResults` array contributes to `total_issues`.
1581pub fn counted_result_issue_metas() -> impl Iterator<Item = &'static IssueResultMeta> {
1582    result_issue_metas().filter(|meta| meta.counts_in_total)
1583}
1584
1585#[cfg(test)]
1586mod tests {
1587    use std::collections::BTreeSet;
1588
1589    use crate::results::TOTAL_ISSUE_RESULT_KEYS;
1590
1591    use super::*;
1592
1593    #[test]
1594    fn known_names_round_trip_through_metadata() {
1595        for name in KNOWN_ISSUE_KIND_NAMES {
1596            let meta = issue_meta_for_token(name)
1597                .unwrap_or_else(|| panic!("known issue name {name} missing metadata row"));
1598            assert!(
1599                meta.kind.is_some(),
1600                "known issue name {name} maps to non-IssueKind metadata"
1601            );
1602        }
1603    }
1604
1605    #[test]
1606    fn issue_kind_variants_have_metadata() {
1607        for &kind in IssueKind::ALL {
1608            assert!(
1609                issue_meta_by_kind(kind).is_some(),
1610                "IssueKind {kind:?} has no metadata row"
1611            );
1612        }
1613    }
1614
1615    #[test]
1616    fn dead_code_filter_flags_match_metadata() {
1617        let from_constants: BTreeSet<&str> = DEAD_CODE_FILTER_FLAGS.iter().copied().collect();
1618        let from_meta: BTreeSet<&str> = ISSUE_KIND_META
1619            .iter()
1620            .filter_map(|meta| meta.filter_flag)
1621            .collect();
1622        assert_eq!(from_constants, from_meta);
1623    }
1624
1625    #[test]
1626    fn mcp_issue_type_flags_match_metadata() {
1627        let from_constants: BTreeSet<(&str, &str)> = MCP_ISSUE_TYPE_FLAGS.iter().copied().collect();
1628        let from_meta: BTreeSet<(&str, &str)> = ISSUE_KIND_META
1629            .iter()
1630            .filter_map(|meta| meta.mcp_pair())
1631            .collect();
1632        assert_eq!(from_constants, from_meta);
1633    }
1634
1635    #[test]
1636    fn lsp_exposes_only_actual_diagnostic_codes() {
1637        let codes: BTreeSet<&str> = diagnostic_issue_metas().map(|meta| meta.code).collect();
1638        assert!(codes.contains("boundary-violation"));
1639        assert!(!codes.contains("boundary-coverage"));
1640        assert!(!codes.contains("boundary-call-violation"));
1641    }
1642
1643    #[test]
1644    fn issue_codes_are_unique() {
1645        let mut seen = BTreeSet::new();
1646        for meta in ISSUE_KIND_META {
1647            assert!(seen.insert(meta.code), "duplicate issue code {}", meta.code);
1648        }
1649    }
1650
1651    #[test]
1652    fn result_meta_codes_have_issue_metadata() {
1653        for meta in ISSUE_RESULT_META {
1654            assert!(
1655                issue_meta_by_code(meta.code).is_some(),
1656                "result metadata code {} has no issue metadata row",
1657                meta.code
1658            );
1659        }
1660    }
1661
1662    #[test]
1663    fn result_meta_codes_have_docs_anchors() {
1664        for meta in ISSUE_RESULT_META {
1665            let issue = issue_meta_by_code(meta.code)
1666                .unwrap_or_else(|| panic!("result metadata code {} has no issue row", meta.code));
1667            assert_eq!(
1668                issue.docs_anchor(),
1669                Some(meta.docs_anchor),
1670                "result metadata code {} has mismatched docs anchor",
1671                meta.code
1672            );
1673        }
1674    }
1675
1676    #[test]
1677    fn result_meta_codes_have_summary_labels() {
1678        for meta in ISSUE_RESULT_META {
1679            assert!(
1680                !meta.summary_label.is_empty(),
1681                "result metadata code {} has no summary label",
1682                meta.code
1683            );
1684        }
1685    }
1686
1687    #[test]
1688    fn result_meta_codes_have_meta_names() {
1689        for meta in ISSUE_RESULT_META {
1690            assert!(
1691                !meta.meta_name.is_empty(),
1692                "result metadata code {} has no meta name",
1693                meta.code
1694            );
1695        }
1696    }
1697
1698    #[test]
1699    fn result_meta_codes_have_meta_docs_paths() {
1700        for meta in ISSUE_RESULT_META {
1701            assert!(
1702                meta.meta_docs_path.starts_with("explanations/dead-code#"),
1703                "result metadata code {} has invalid meta docs path",
1704                meta.code
1705            );
1706        }
1707    }
1708
1709    #[test]
1710    fn result_meta_codes_have_meta_descriptions() {
1711        for meta in ISSUE_RESULT_META {
1712            assert!(
1713                !meta.meta_description.is_empty(),
1714                "result metadata code {} has no meta description",
1715                meta.code
1716            );
1717        }
1718    }
1719
1720    #[test]
1721    fn ci_format_ids_are_prefixed_and_known() {
1722        let result_codes: BTreeSet<&str> = result_issue_metas().map(|meta| meta.code).collect();
1723        let codeclimate_codes: BTreeSet<&str> = CODECLIMATE_RESULT_CODES.iter().copied().collect();
1724        assert!(codeclimate_codes.is_subset(&result_codes));
1725
1726        for meta in result_issue_metas() {
1727            let issue = issue_meta_by_code(meta.code)
1728                .unwrap_or_else(|| panic!("result metadata code {} has no issue row", meta.code));
1729            assert!(issue.sarif_enabled());
1730            let sarif_ids = issue.sarif_rule_ids();
1731            assert!(sarif_ids.contains(&format!("fallow/{}", meta.code)));
1732            for rule_id in sarif_ids {
1733                assert!(
1734                    rule_id.starts_with("fallow/"),
1735                    "result metadata code {} has unprefixed SARIF rule id {rule_id}",
1736                    meta.code
1737                );
1738            }
1739            for check_name in issue.codeclimate_check_names() {
1740                assert!(
1741                    check_name.starts_with("fallow/"),
1742                    "result metadata code {} has unprefixed CodeClimate check name {check_name}",
1743                    meta.code
1744                );
1745            }
1746        }
1747    }
1748
1749    #[test]
1750    fn ts_alias_policy_is_explicit() {
1751        let aliases: BTreeSet<(&str, &str)> = result_issue_metas()
1752            .filter_map(|meta| {
1753                issue_meta_by_code(meta.code)
1754                    .and_then(|issue| issue.ts_alias())
1755                    .map(|alias| (alias.name, alias.parent))
1756            })
1757            .collect();
1758
1759        assert_eq!(
1760            BTreeSet::from([
1761                ("BoundaryViolation", "BoundaryViolationFinding"),
1762                ("CircularDependency", "CircularDependencyFinding"),
1763                ("DuplicateExport", "DuplicateExportFinding"),
1764                ("EmptyCatalogGroup", "EmptyCatalogGroupFinding"),
1765                (
1766                    "MisconfiguredDependencyOverride",
1767                    "MisconfiguredDependencyOverrideFinding",
1768                ),
1769                ("PrivateTypeLeak", "PrivateTypeLeakFinding"),
1770                ("ReExportCycle", "ReExportCycleFinding"),
1771                ("TestOnlyDependency", "TestOnlyDependencyFinding"),
1772                ("TypeOnlyDependency", "TypeOnlyDependencyFinding"),
1773                (
1774                    "UnresolvedCatalogReference",
1775                    "UnresolvedCatalogReferenceFinding",
1776                ),
1777                ("UnresolvedImport", "UnresolvedImportFinding"),
1778                ("UnlistedDependency", "UnlistedDependencyFinding"),
1779                ("UnusedCatalogEntry", "UnusedCatalogEntryFinding"),
1780                ("UnusedDependency", "UnusedDependencyFinding"),
1781                ("UnusedDependency", "UnusedDevDependencyFinding"),
1782                ("UnusedDependency", "UnusedOptionalDependencyFinding"),
1783                (
1784                    "UnusedDependencyOverride",
1785                    "UnusedDependencyOverrideFinding",
1786                ),
1787                ("UnusedExport", "UnusedExportFinding"),
1788                ("UnusedFile", "UnusedFileFinding"),
1789                ("UnusedMember", "UnusedClassMemberFinding"),
1790                ("UnusedMember", "UnusedEnumMemberFinding"),
1791                ("UnusedMember", "UnusedStoreMemberFinding"),
1792            ]),
1793            aliases
1794        );
1795    }
1796
1797    #[test]
1798    fn result_keys_are_unique() {
1799        let mut seen = BTreeSet::new();
1800        for meta in ISSUE_RESULT_META {
1801            assert!(
1802                seen.insert(meta.result_key),
1803                "duplicate result key {}",
1804                meta.result_key
1805            );
1806        }
1807    }
1808
1809    #[test]
1810    fn counted_result_keys_match_total_issue_fields() {
1811        let from_total: BTreeSet<&str> = TOTAL_ISSUE_RESULT_KEYS.iter().copied().collect();
1812        let from_meta: BTreeSet<&str> = counted_result_issue_metas()
1813            .map(|meta| meta.result_key)
1814            .collect();
1815        assert_eq!(from_total, from_meta);
1816    }
1817
1818    #[test]
1819    fn advisory_result_keys_are_explicitly_excluded_from_total() {
1820        let expected = BTreeSet::from([
1821            "duplicate_prop_shapes",
1822            "prop_drilling_chains",
1823            "thin_wrappers",
1824        ]);
1825        let from_meta: BTreeSet<&str> = result_issue_metas()
1826            .filter(|meta| !meta.counts_in_total)
1827            .map(|meta| meta.result_key)
1828            .collect();
1829        assert_eq!(expected, from_meta);
1830    }
1831}