1use std::sync::atomic::{AtomicBool, Ordering};
2
3use fallow_config::{ResolvedConfig, RulesConfig, Severity};
4use rustc_hash::FxHashMap;
5
6pub use fallow_types::suppress::{IssueKind, Suppression, UnknownSuppressionKind};
8
9pub use fallow_extract::suppress::parse_suppressions_from_source;
11
12use crate::discover::FileId;
13use crate::extract::ModuleInfo;
14use crate::graph::ModuleGraph;
15use crate::results::{StaleSuppression, SuppressionOrigin};
16
17fn severity_for_kind(rules: &RulesConfig, kind: IssueKind) -> Severity {
26 match kind {
27 IssueKind::UnusedFile => rules.unused_files,
28 IssueKind::UnusedExport => rules.unused_exports,
29 IssueKind::UnusedType => rules.unused_types,
30 IssueKind::PrivateTypeLeak => rules.private_type_leaks,
31 IssueKind::UnusedDependency => rules.unused_dependencies,
32 IssueKind::UnusedDevDependency => rules.unused_dev_dependencies,
33 IssueKind::UnusedEnumMember => rules.unused_enum_members,
34 IssueKind::UnusedClassMember => rules.unused_class_members,
35 IssueKind::UnresolvedImport => rules.unresolved_imports,
36 IssueKind::UnlistedDependency => rules.unlisted_dependencies,
37 IssueKind::DuplicateExport => rules.duplicate_exports,
38 IssueKind::CircularDependency => rules.circular_dependencies,
39 IssueKind::ReExportCycle => rules.re_export_cycle,
40 IssueKind::TypeOnlyDependency => rules.type_only_dependencies,
41 IssueKind::TestOnlyDependency => rules.test_only_dependencies,
42 IssueKind::BoundaryViolation => rules.boundary_violation,
43 IssueKind::CoverageGaps => rules.coverage_gaps,
44 IssueKind::FeatureFlag => rules.feature_flags,
45 IssueKind::StaleSuppression => rules.stale_suppressions,
46 IssueKind::PnpmCatalogEntry => rules.unused_catalog_entries,
47 IssueKind::EmptyCatalogGroup => rules.empty_catalog_groups,
48 IssueKind::UnresolvedCatalogReference => rules.unresolved_catalog_references,
49 IssueKind::UnusedDependencyOverride => rules.unused_dependency_overrides,
50 IssueKind::MisconfiguredDependencyOverride => rules.misconfigured_dependency_overrides,
51 IssueKind::Complexity | IssueKind::CodeDuplication => Severity::Error,
53 }
54}
55
56const NON_CORE_KINDS: &[IssueKind] = &[
62 IssueKind::Complexity,
64 IssueKind::CoverageGaps,
65 IssueKind::FeatureFlag,
66 IssueKind::CodeDuplication,
67 IssueKind::UnusedDependency,
69 IssueKind::UnusedDevDependency,
70 IssueKind::UnlistedDependency,
71 IssueKind::TypeOnlyDependency,
72 IssueKind::TestOnlyDependency,
73 IssueKind::PnpmCatalogEntry,
74 IssueKind::EmptyCatalogGroup,
75 IssueKind::UnresolvedCatalogReference,
76 IssueKind::UnusedDependencyOverride,
77 IssueKind::MisconfiguredDependencyOverride,
78 IssueKind::StaleSuppression,
82];
83
84pub struct SuppressionContext<'a> {
93 by_file: FxHashMap<FileId, &'a [Suppression]>,
94 used: FxHashMap<FileId, Vec<AtomicBool>>,
95 unknown_kinds: FxHashMap<FileId, &'a [UnknownSuppressionKind]>,
99}
100
101impl<'a> SuppressionContext<'a> {
102 pub fn new(modules: &'a [ModuleInfo]) -> Self {
104 let by_file: FxHashMap<FileId, &[Suppression]> = modules
105 .iter()
106 .filter(|m| !m.suppressions.is_empty())
107 .map(|m| (m.file_id, m.suppressions.as_slice()))
108 .collect();
109
110 let used = by_file
111 .iter()
112 .map(|(&fid, supps)| {
113 (
114 fid,
115 std::iter::repeat_with(|| AtomicBool::new(false))
116 .take(supps.len())
117 .collect(),
118 )
119 })
120 .collect();
121
122 let unknown_kinds: FxHashMap<FileId, &[UnknownSuppressionKind]> = modules
123 .iter()
124 .filter(|m| !m.unknown_suppression_kinds.is_empty())
125 .map(|m| (m.file_id, m.unknown_suppression_kinds.as_slice()))
126 .collect();
127
128 Self {
129 by_file,
130 used,
131 unknown_kinds,
132 }
133 }
134
135 #[cfg(test)]
137 pub fn from_map(by_file: FxHashMap<FileId, &'a [Suppression]>) -> Self {
138 let used = by_file
139 .iter()
140 .map(|(&fid, supps)| {
141 (
142 fid,
143 std::iter::repeat_with(|| AtomicBool::new(false))
144 .take(supps.len())
145 .collect(),
146 )
147 })
148 .collect();
149 Self {
150 by_file,
151 used,
152 unknown_kinds: FxHashMap::default(),
153 }
154 }
155
156 #[cfg(test)]
158 pub fn empty() -> Self {
159 Self {
160 by_file: FxHashMap::default(),
161 used: FxHashMap::default(),
162 unknown_kinds: FxHashMap::default(),
163 }
164 }
165
166 #[must_use]
169 pub fn is_suppressed(&self, file_id: FileId, line: u32, kind: IssueKind) -> bool {
170 let Some(supps) = self.by_file.get(&file_id) else {
171 return false;
172 };
173 let Some(used) = self.used.get(&file_id) else {
174 return false;
175 };
176 for (i, s) in supps.iter().enumerate() {
177 let matched = if s.line == 0 {
178 s.kind.is_none() || s.kind == Some(kind)
179 } else {
180 s.line == line && (s.kind.is_none() || s.kind == Some(kind))
181 };
182 if matched {
183 used[i].store(true, Ordering::Relaxed);
184 return true;
185 }
186 }
187 false
188 }
189
190 #[must_use]
193 pub fn is_file_suppressed(&self, file_id: FileId, kind: IssueKind) -> bool {
194 let Some(supps) = self.by_file.get(&file_id) else {
195 return false;
196 };
197 let Some(used) = self.used.get(&file_id) else {
198 return false;
199 };
200 for (i, s) in supps.iter().enumerate() {
201 if s.line == 0 && (s.kind.is_none() || s.kind == Some(kind)) {
202 used[i].store(true, Ordering::Relaxed);
203 return true;
204 }
205 }
206 false
207 }
208
209 pub fn get(&self, file_id: FileId) -> Option<&[Suppression]> {
211 self.by_file.get(&file_id).copied()
212 }
213
214 #[must_use]
216 pub fn used_count(&self) -> usize {
217 self.used
218 .values()
219 .flat_map(|used| used.iter())
220 .filter(|used| used.load(Ordering::Relaxed))
221 .count()
222 }
223
224 pub fn find_stale(
235 &self,
236 graph: &ModuleGraph,
237 config: &ResolvedConfig,
238 ) -> Vec<StaleSuppression> {
239 let mut stale = Vec::new();
240
241 for (&file_id, supps) in &self.by_file {
242 let used = &self.used[&file_id];
243 let path = &graph.modules[file_id.0 as usize].path;
244 let file_rules = config.resolve_rules_for_path(path);
247
248 for (i, s) in supps.iter().enumerate() {
249 if used[i].load(Ordering::Relaxed) {
250 continue;
251 }
252
253 if let Some(kind) = s.kind
257 && NON_CORE_KINDS.contains(&kind)
258 {
259 continue;
260 }
261
262 if let Some(kind) = s.kind
268 && severity_for_kind(&file_rules, kind) == Severity::Off
269 {
270 continue;
271 }
272
273 let is_file_level = s.line == 0;
274 let issue_kind_str = s.kind.map(|k| {
275 match k {
277 IssueKind::UnusedFile => "unused-file",
278 IssueKind::UnusedExport => "unused-export",
279 IssueKind::UnusedType => "unused-type",
280 IssueKind::PrivateTypeLeak => "private-type-leak",
281 IssueKind::UnusedDependency => "unused-dependency",
282 IssueKind::UnusedDevDependency => "unused-dev-dependency",
283 IssueKind::UnusedEnumMember => "unused-enum-member",
284 IssueKind::UnusedClassMember => "unused-class-member",
285 IssueKind::UnresolvedImport => "unresolved-import",
286 IssueKind::UnlistedDependency => "unlisted-dependency",
287 IssueKind::DuplicateExport => "duplicate-export",
288 IssueKind::CodeDuplication => "code-duplication",
289 IssueKind::CircularDependency => "circular-dependency",
290 IssueKind::ReExportCycle => "re-export-cycle",
291 IssueKind::TypeOnlyDependency => "type-only-dependency",
292 IssueKind::TestOnlyDependency => "test-only-dependency",
293 IssueKind::BoundaryViolation => "boundary-violation",
294 IssueKind::CoverageGaps => "coverage-gaps",
295 IssueKind::FeatureFlag => "feature-flag",
296 IssueKind::Complexity => "complexity",
297 IssueKind::StaleSuppression => "stale-suppression",
298 IssueKind::PnpmCatalogEntry => "unused-catalog-entry",
299 IssueKind::EmptyCatalogGroup => "empty-catalog-group",
300 IssueKind::UnresolvedCatalogReference => "unresolved-catalog-reference",
301 IssueKind::UnusedDependencyOverride => "unused-dependency-override",
302 IssueKind::MisconfiguredDependencyOverride => {
303 "misconfigured-dependency-override"
304 }
305 }
306 .to_string()
307 });
308
309 stale.push(StaleSuppression {
310 path: path.clone(),
311 line: s.comment_line,
312 col: 0,
313 origin: SuppressionOrigin::Comment {
314 issue_kind: issue_kind_str,
315 is_file_level,
316 kind_known: true,
317 },
318 });
319 }
320 }
321
322 for (&file_id, unknowns) in &self.unknown_kinds {
327 let path = &graph.modules[file_id.0 as usize].path;
328 for u in *unknowns {
329 stale.push(StaleSuppression {
330 path: path.clone(),
331 line: u.comment_line,
332 col: 0,
333 origin: SuppressionOrigin::Comment {
334 issue_kind: Some(u.token.clone()),
335 is_file_level: u.is_file_level,
336 kind_known: false,
337 },
338 });
339 }
340 }
341
342 stale
343 }
344}
345
346#[must_use]
351pub fn is_suppressed(suppressions: &[Suppression], line: u32, kind: IssueKind) -> bool {
352 suppressions.iter().any(|s| {
353 if s.line == 0 {
355 return s.kind.is_none() || s.kind == Some(kind);
356 }
357 s.line == line && (s.kind.is_none() || s.kind == Some(kind))
359 })
360}
361
362#[must_use]
366pub fn is_file_suppressed(suppressions: &[Suppression], kind: IssueKind) -> bool {
367 suppressions
368 .iter()
369 .any(|s| s.line == 0 && (s.kind.is_none() || s.kind == Some(kind)))
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn severity_for_kind_maps_every_core_kind_to_its_field() {
378 let rules = RulesConfig {
379 unused_exports: Severity::Warn,
380 unused_types: Severity::Off,
381 unresolved_imports: Severity::Error,
382 boundary_violation: Severity::Off,
383 ..RulesConfig::default()
384 };
385
386 assert_eq!(
387 severity_for_kind(&rules, IssueKind::UnusedExport),
388 Severity::Warn
389 );
390 assert_eq!(
391 severity_for_kind(&rules, IssueKind::UnusedType),
392 Severity::Off
393 );
394 assert_eq!(
395 severity_for_kind(&rules, IssueKind::UnresolvedImport),
396 Severity::Error
397 );
398 assert_eq!(
399 severity_for_kind(&rules, IssueKind::BoundaryViolation),
400 Severity::Off
401 );
402 assert_eq!(
404 severity_for_kind(&rules, IssueKind::PrivateTypeLeak),
405 Severity::Off
406 );
407 }
408
409 #[test]
410 fn issue_kind_from_str_all_variants() {
411 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
412 assert_eq!(
413 IssueKind::parse("unused-export"),
414 Some(IssueKind::UnusedExport)
415 );
416 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
417 assert_eq!(
418 IssueKind::parse("unused-dependency"),
419 Some(IssueKind::UnusedDependency)
420 );
421 assert_eq!(
422 IssueKind::parse("unused-dev-dependency"),
423 Some(IssueKind::UnusedDevDependency)
424 );
425 assert_eq!(
426 IssueKind::parse("unused-enum-member"),
427 Some(IssueKind::UnusedEnumMember)
428 );
429 assert_eq!(
430 IssueKind::parse("unused-class-member"),
431 Some(IssueKind::UnusedClassMember)
432 );
433 assert_eq!(
434 IssueKind::parse("unresolved-import"),
435 Some(IssueKind::UnresolvedImport)
436 );
437 assert_eq!(
438 IssueKind::parse("unlisted-dependency"),
439 Some(IssueKind::UnlistedDependency)
440 );
441 assert_eq!(
442 IssueKind::parse("duplicate-export"),
443 Some(IssueKind::DuplicateExport)
444 );
445 }
446
447 #[test]
448 fn issue_kind_from_str_unknown() {
449 assert_eq!(IssueKind::parse("foo"), None);
450 assert_eq!(IssueKind::parse(""), None);
451 }
452
453 #[test]
454 fn discriminant_roundtrip() {
455 for kind in [
456 IssueKind::UnusedFile,
457 IssueKind::UnusedExport,
458 IssueKind::UnusedType,
459 IssueKind::PrivateTypeLeak,
460 IssueKind::UnusedDependency,
461 IssueKind::UnusedDevDependency,
462 IssueKind::UnusedEnumMember,
463 IssueKind::UnusedClassMember,
464 IssueKind::UnresolvedImport,
465 IssueKind::UnlistedDependency,
466 IssueKind::DuplicateExport,
467 IssueKind::CodeDuplication,
468 IssueKind::CircularDependency,
469 IssueKind::TestOnlyDependency,
470 IssueKind::BoundaryViolation,
471 IssueKind::CoverageGaps,
472 IssueKind::FeatureFlag,
473 IssueKind::Complexity,
474 IssueKind::StaleSuppression,
475 IssueKind::PnpmCatalogEntry,
476 IssueKind::EmptyCatalogGroup,
477 IssueKind::UnresolvedCatalogReference,
478 IssueKind::UnusedDependencyOverride,
479 IssueKind::MisconfiguredDependencyOverride,
480 IssueKind::ReExportCycle,
481 ] {
482 assert_eq!(
483 IssueKind::from_discriminant(kind.to_discriminant()),
484 Some(kind)
485 );
486 }
487 assert_eq!(IssueKind::from_discriminant(0), None);
488 assert_eq!(IssueKind::from_discriminant(27), None);
489 }
490
491 #[test]
492 fn parse_file_wide_suppression() {
493 let source = "// fallow-ignore-file\nexport const foo = 1;\n";
494 let suppressions = parse_suppressions_from_source(source).suppressions;
495 assert_eq!(suppressions.len(), 1);
496 assert_eq!(suppressions[0].line, 0);
497 assert!(suppressions[0].kind.is_none());
498 }
499
500 #[test]
501 fn parse_file_wide_suppression_with_kind() {
502 let source = "// fallow-ignore-file unused-export\nexport const foo = 1;\n";
503 let suppressions = parse_suppressions_from_source(source).suppressions;
504 assert_eq!(suppressions.len(), 1);
505 assert_eq!(suppressions[0].line, 0);
506 assert_eq!(suppressions[0].kind, Some(IssueKind::UnusedExport));
507 }
508
509 #[test]
510 fn parse_next_line_suppression() {
511 let source =
512 "import { x } from './x';\n// fallow-ignore-next-line\nexport const foo = 1;\n";
513 let suppressions = parse_suppressions_from_source(source).suppressions;
514 assert_eq!(suppressions.len(), 1);
515 assert_eq!(suppressions[0].line, 3); assert!(suppressions[0].kind.is_none());
517 }
518
519 #[test]
520 fn parse_next_line_suppression_with_kind() {
521 let source = "// fallow-ignore-next-line unused-export\nexport const foo = 1;\n";
522 let suppressions = parse_suppressions_from_source(source).suppressions;
523 assert_eq!(suppressions.len(), 1);
524 assert_eq!(suppressions[0].line, 2);
525 assert_eq!(suppressions[0].kind, Some(IssueKind::UnusedExport));
526 }
527
528 #[test]
529 fn parse_unknown_kind_surfaces_as_unknown() {
530 let source = "// fallow-ignore-next-line typo-kind\nexport const foo = 1;\n";
531 let parsed = parse_suppressions_from_source(source);
532 assert!(parsed.suppressions.is_empty());
534 assert_eq!(parsed.unknown_kinds.len(), 1);
537 assert_eq!(parsed.unknown_kinds[0].token, "typo-kind");
538 }
539
540 #[test]
541 fn is_suppressed_file_wide() {
542 let suppressions = vec![Suppression {
543 line: 0,
544 comment_line: 1,
545 kind: None,
546 }];
547 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
548 assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedFile));
549 }
550
551 #[test]
552 fn is_suppressed_file_wide_specific_kind() {
553 let suppressions = vec![Suppression {
554 line: 0,
555 comment_line: 1,
556 kind: Some(IssueKind::UnusedExport),
557 }];
558 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
559 assert!(!is_suppressed(&suppressions, 5, IssueKind::UnusedType));
560 }
561
562 #[test]
563 fn is_suppressed_line_specific() {
564 let suppressions = vec![Suppression {
565 line: 5,
566 comment_line: 4,
567 kind: None,
568 }];
569 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
570 assert!(!is_suppressed(&suppressions, 6, IssueKind::UnusedExport));
571 }
572
573 #[test]
574 fn is_suppressed_line_and_kind() {
575 let suppressions = vec![Suppression {
576 line: 5,
577 comment_line: 4,
578 kind: Some(IssueKind::UnusedExport),
579 }];
580 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
581 assert!(!is_suppressed(&suppressions, 5, IssueKind::UnusedType));
582 assert!(!is_suppressed(&suppressions, 6, IssueKind::UnusedExport));
583 }
584
585 #[test]
586 fn is_suppressed_empty() {
587 assert!(!is_suppressed(&[], 5, IssueKind::UnusedExport));
588 }
589
590 #[test]
591 fn is_file_suppressed_works() {
592 let suppressions = vec![Suppression {
593 line: 0,
594 comment_line: 1,
595 kind: None,
596 }];
597 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
598
599 let suppressions = vec![Suppression {
600 line: 0,
601 comment_line: 1,
602 kind: Some(IssueKind::UnusedFile),
603 }];
604 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
605 assert!(!is_file_suppressed(&suppressions, IssueKind::UnusedExport));
606
607 let suppressions = vec![Suppression {
609 line: 5,
610 comment_line: 4,
611 kind: None,
612 }];
613 assert!(!is_file_suppressed(&suppressions, IssueKind::UnusedFile));
614 }
615
616 #[test]
617 fn parse_oxc_comments() {
618 use fallow_extract::suppress::parse_suppressions;
619 use oxc_allocator::Allocator;
620 use oxc_parser::Parser;
621 use oxc_span::SourceType;
622
623 let source = "// fallow-ignore-file\n// fallow-ignore-next-line unused-export\nexport const foo = 1;\nexport const bar = 2;\n";
624 let allocator = Allocator::default();
625 let parser_return = Parser::new(&allocator, source, SourceType::mjs()).parse();
626
627 let suppressions = parse_suppressions(&parser_return.program.comments, source).suppressions;
628 assert_eq!(suppressions.len(), 2);
629
630 assert_eq!(suppressions[0].line, 0);
632 assert!(suppressions[0].kind.is_none());
633
634 assert_eq!(suppressions[1].line, 3); assert_eq!(suppressions[1].kind, Some(IssueKind::UnusedExport));
637 }
638
639 #[test]
640 fn parse_block_comment_suppression() {
641 let source = "/* fallow-ignore-file */\nexport const foo = 1;\n";
642 let suppressions = parse_suppressions_from_source(source).suppressions;
643 assert_eq!(suppressions.len(), 1);
644 assert_eq!(suppressions[0].line, 0);
645 assert!(suppressions[0].kind.is_none());
646 }
647
648 #[test]
649 fn is_suppressed_multiple_suppressions_different_kinds() {
650 let suppressions = vec![
651 Suppression {
652 line: 5,
653 comment_line: 4,
654 kind: Some(IssueKind::UnusedExport),
655 },
656 Suppression {
657 line: 5,
658 comment_line: 4,
659 kind: Some(IssueKind::UnusedType),
660 },
661 ];
662 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
663 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedType));
664 assert!(!is_suppressed(&suppressions, 5, IssueKind::UnusedFile));
665 }
666
667 #[test]
668 fn is_suppressed_file_wide_blanket_and_specific_coexist() {
669 let suppressions = vec![
670 Suppression {
671 line: 0,
672 comment_line: 1,
673 kind: Some(IssueKind::UnusedExport),
674 },
675 Suppression {
676 line: 5,
677 comment_line: 4,
678 kind: None, },
680 ];
681 assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedExport));
683 assert!(!is_suppressed(&suppressions, 10, IssueKind::UnusedType));
684
685 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedType));
687 assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
688 }
689
690 #[test]
691 fn is_file_suppressed_blanket_suppresses_all_kinds() {
692 let suppressions = vec![Suppression {
693 line: 0,
694 comment_line: 1,
695 kind: None, }];
697 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
698 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedExport));
699 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedType));
700 assert!(is_file_suppressed(
701 &suppressions,
702 IssueKind::CircularDependency
703 ));
704 assert!(is_file_suppressed(
705 &suppressions,
706 IssueKind::CodeDuplication
707 ));
708 }
709
710 #[test]
711 fn is_file_suppressed_empty_list() {
712 assert!(!is_file_suppressed(&[], IssueKind::UnusedFile));
713 }
714
715 #[test]
716 fn parse_multiple_next_line_suppressions() {
717 let source = "// fallow-ignore-next-line unused-export\nexport const foo = 1;\n// fallow-ignore-next-line unused-type\nexport type Bar = string;\n";
718 let suppressions = parse_suppressions_from_source(source).suppressions;
719 assert_eq!(suppressions.len(), 2);
720 assert_eq!(suppressions[0].line, 2);
721 assert_eq!(suppressions[0].kind, Some(IssueKind::UnusedExport));
722 assert_eq!(suppressions[1].line, 4);
723 assert_eq!(suppressions[1].kind, Some(IssueKind::UnusedType));
724 }
725
726 #[test]
727 fn parse_code_duplication_suppression() {
728 let source = "// fallow-ignore-file code-duplication\nexport const foo = 1;\n";
729 let suppressions = parse_suppressions_from_source(source).suppressions;
730 assert_eq!(suppressions.len(), 1);
731 assert_eq!(suppressions[0].line, 0);
732 assert_eq!(suppressions[0].kind, Some(IssueKind::CodeDuplication));
733 }
734
735 #[test]
736 fn parse_circular_dependency_suppression() {
737 let source = "// fallow-ignore-file circular-dependency\nimport { x } from './x';\n";
738 let suppressions = parse_suppressions_from_source(source).suppressions;
739 assert_eq!(suppressions.len(), 1);
740 assert_eq!(suppressions[0].line, 0);
741 assert_eq!(suppressions[0].kind, Some(IssueKind::CircularDependency));
742 }
743
744 #[test]
750 fn all_issue_kinds_classified_for_stale_detection() {
751 let core_kinds = [
753 IssueKind::UnusedFile,
754 IssueKind::UnusedExport,
755 IssueKind::UnusedType,
756 IssueKind::UnusedEnumMember,
757 IssueKind::UnusedClassMember,
758 IssueKind::UnresolvedImport,
759 IssueKind::DuplicateExport,
760 IssueKind::CircularDependency,
761 IssueKind::BoundaryViolation,
762 ];
763
764 let all_kinds = [
766 IssueKind::UnusedFile,
767 IssueKind::UnusedExport,
768 IssueKind::UnusedType,
769 IssueKind::UnusedDependency,
770 IssueKind::UnusedDevDependency,
771 IssueKind::UnusedEnumMember,
772 IssueKind::UnusedClassMember,
773 IssueKind::UnresolvedImport,
774 IssueKind::UnlistedDependency,
775 IssueKind::DuplicateExport,
776 IssueKind::CodeDuplication,
777 IssueKind::CircularDependency,
778 IssueKind::TypeOnlyDependency,
779 IssueKind::TestOnlyDependency,
780 IssueKind::BoundaryViolation,
781 IssueKind::CoverageGaps,
782 IssueKind::FeatureFlag,
783 IssueKind::Complexity,
784 IssueKind::StaleSuppression,
785 IssueKind::PnpmCatalogEntry,
786 IssueKind::EmptyCatalogGroup,
787 IssueKind::UnresolvedCatalogReference,
788 IssueKind::UnusedDependencyOverride,
789 IssueKind::MisconfiguredDependencyOverride,
790 ];
791
792 for kind in all_kinds {
793 let in_core = core_kinds.contains(&kind);
794 let in_non_core = NON_CORE_KINDS.contains(&kind);
795 assert!(
796 in_core || in_non_core,
797 "IssueKind::{kind:?} is not classified in either core_kinds or NON_CORE_KINDS. \
798 Add it to NON_CORE_KINDS if it is checked outside find_dead_code_full, \
799 or to core_kinds in this test if a core detector checks it."
800 );
801 assert!(
802 !(in_core && in_non_core),
803 "IssueKind::{kind:?} is in BOTH core_kinds and NON_CORE_KINDS. Pick one."
804 );
805 }
806 }
807}