Skip to main content

fallow_types/
issue_meta.rs

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