1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::sync::Arc;
7use std::time::{Duration, SystemTime};
8
9use serde::{Deserialize, Serialize};
10
11use crate::cache_freshness::FileFreshness;
12use crate::config::Config;
13use crate::parser::{LangId, SharedSymbolCache};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum InspectCategory {
18 Diagnostics,
19 Metrics,
20 Todos,
21 DeadCode,
22 UnusedExports,
23 Duplicates,
24 Cycles,
25 Complexity,
26 CircularDeps,
27 OutdatedDeps,
28 Vulnerabilities,
29 TestCoverageGaps,
30 ApiSurface,
31}
32
33impl InspectCategory {
34 pub const ACTIVE: [InspectCategory; 7] = [
35 InspectCategory::Diagnostics,
36 InspectCategory::Metrics,
37 InspectCategory::Todos,
38 InspectCategory::DeadCode,
39 InspectCategory::UnusedExports,
40 InspectCategory::Duplicates,
41 InspectCategory::Cycles,
42 ];
43
44 pub const DISABLED: [InspectCategory; 6] = [
45 InspectCategory::Complexity,
46 InspectCategory::CircularDeps,
47 InspectCategory::OutdatedDeps,
48 InspectCategory::Vulnerabilities,
49 InspectCategory::TestCoverageGaps,
50 InspectCategory::ApiSurface,
51 ];
52
53 pub fn as_str(self) -> &'static str {
54 match self {
55 InspectCategory::Diagnostics => "diagnostics",
56 InspectCategory::Metrics => "metrics",
57 InspectCategory::Todos => "todos",
58 InspectCategory::DeadCode => "dead_code",
59 InspectCategory::UnusedExports => "unused_exports",
60 InspectCategory::Duplicates => "duplicates",
61 InspectCategory::Cycles => "cycles",
62 InspectCategory::Complexity => "complexity",
63 InspectCategory::CircularDeps => "circular_deps",
64 InspectCategory::OutdatedDeps => "outdated_deps",
65 InspectCategory::Vulnerabilities => "vulnerabilities",
66 InspectCategory::TestCoverageGaps => "test_coverage_gaps",
67 InspectCategory::ApiSurface => "api_surface",
68 }
69 }
70
71 pub fn tier(self) -> InspectTier {
72 match self {
73 InspectCategory::Diagnostics | InspectCategory::Metrics | InspectCategory::Todos => {
74 InspectTier::Tier1
75 }
76 InspectCategory::DeadCode
77 | InspectCategory::UnusedExports
78 | InspectCategory::Duplicates
79 | InspectCategory::Cycles
80 | InspectCategory::Complexity
81 | InspectCategory::CircularDeps
82 | InspectCategory::ApiSurface => InspectTier::Tier2,
83 InspectCategory::OutdatedDeps
84 | InspectCategory::Vulnerabilities
85 | InspectCategory::TestCoverageGaps => InspectTier::Tier3,
86 }
87 }
88
89 pub fn is_tier2(self) -> bool {
90 self.tier() == InspectTier::Tier2
91 }
92
93 pub fn is_active(self) -> bool {
94 Self::ACTIVE.contains(&self)
95 }
96
97 pub fn active() -> &'static [InspectCategory] {
98 &Self::ACTIVE
99 }
100
101 pub fn disabled() -> &'static [InspectCategory] {
102 &Self::DISABLED
103 }
104}
105
106impl fmt::Display for InspectCategory {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 formatter.write_str(self.as_str())
109 }
110}
111
112impl FromStr for InspectCategory {
113 type Err = InspectCategoryParseError;
114
115 fn from_str(value: &str) -> Result<Self, Self::Err> {
116 match value {
117 "diagnostics" => Ok(Self::Diagnostics),
118 "metrics" => Ok(Self::Metrics),
119 "todos" => Ok(Self::Todos),
120 "dead_code" => Ok(Self::DeadCode),
121 "unused_exports" => Ok(Self::UnusedExports),
122 "duplicates" => Ok(Self::Duplicates),
123 "cycles" => Ok(Self::Cycles),
124 "complexity" => Ok(Self::Complexity),
125 "circular_deps" => Ok(Self::CircularDeps),
126 "outdated_deps" => Ok(Self::OutdatedDeps),
127 "vulnerabilities" => Ok(Self::Vulnerabilities),
128 "test_coverage_gaps" => Ok(Self::TestCoverageGaps),
129 "api_surface" => Ok(Self::ApiSurface),
130 other => Err(InspectCategoryParseError(other.to_string())),
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct InspectCategoryParseError(String);
137
138impl fmt::Display for InspectCategoryParseError {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(formatter, "unknown inspect category '{}'", self.0)
141 }
142}
143
144impl std::error::Error for InspectCategoryParseError {}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum InspectTier {
149 Tier1,
150 Tier2,
151 Tier3,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct JobScope {
156 project_root: PathBuf,
157 roots: Vec<PathBuf>,
158 scope_hash: String,
159}
160
161impl JobScope {
162 pub fn for_project(project_root: impl Into<PathBuf>) -> Self {
163 let project_root = project_root.into();
164 Self {
165 roots: Vec::new(),
166 scope_hash: "project".to_string(),
167 project_root,
168 }
169 }
170
171 pub fn from_roots(project_root: impl Into<PathBuf>, roots: Vec<PathBuf>) -> Self {
172 let project_root = project_root.into();
173 let mut roots = roots
174 .into_iter()
175 .map(|root| normalize_path(&root))
176 .collect::<Vec<_>>();
177 roots.sort();
178 roots.dedup();
179
180 if roots.is_empty() || (roots.len() == 1 && normalize_path(&project_root) == roots[0]) {
181 return Self::for_project(project_root);
182 }
183
184 let mut hasher = std::collections::hash_map::DefaultHasher::new();
185 for root in &roots {
186 root.to_string_lossy().hash(&mut hasher);
187 "\0".hash(&mut hasher);
188 }
189
190 Self {
191 project_root,
192 roots,
193 scope_hash: format!("{:016x}", hasher.finish()),
194 }
195 }
196
197 pub fn project_root(&self) -> &Path {
198 &self.project_root
199 }
200
201 pub fn roots(&self) -> &[PathBuf] {
202 &self.roots
203 }
204
205 pub fn scope_hash(&self) -> &str {
206 &self.scope_hash
207 }
208
209 pub fn is_project_wide(&self) -> bool {
210 self.roots.is_empty()
211 }
212
213 pub fn contains(&self, path: &Path) -> bool {
214 if self.roots.is_empty() {
215 return true;
216 }
217 let normalized = normalize_path(path);
218 self.roots.iter().any(|root| normalized.starts_with(root))
219 }
220
221 pub fn contains_display_path(&self, value: &str) -> bool {
222 let path = PathBuf::from(value);
223 if path.is_absolute() {
224 self.contains(&path)
225 } else {
226 self.contains(&self.project_root.join(path))
227 }
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
232pub struct JobKey {
233 pub category: InspectCategory,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub scope_hash: Option<String>,
236}
237
238impl JobKey {
239 pub fn for_category_scope(category: InspectCategory, scope: &JobScope) -> Self {
240 if category.is_tier2() {
241 Self::for_project_category(category)
242 } else {
243 Self {
244 category,
245 scope_hash: Some(scope.scope_hash().to_string()),
246 }
247 }
248 }
249
250 pub fn for_project_category(category: InspectCategory) -> Self {
251 Self {
252 category,
253 scope_hash: None,
254 }
255 }
256
257 pub fn display_key(&self) -> String {
258 match &self.scope_hash {
259 Some(scope_hash) => format!("{}:{scope_hash}", self.category),
260 None => self.category.to_string(),
261 }
262 }
263}
264
265#[derive(Clone)]
266pub struct InspectSnapshot {
267 pub project_root: PathBuf,
268 pub inspect_dir: PathBuf,
269 pub config: Arc<Config>,
270 pub symbol_cache: SharedSymbolCache,
271 pub inspect_writer: bool,
272 pub callgraph_writer: bool,
273}
274
275impl InspectSnapshot {
276 pub fn new(
277 project_root: PathBuf,
278 inspect_dir: PathBuf,
279 config: Arc<Config>,
280 symbol_cache: SharedSymbolCache,
281 ) -> Self {
282 Self::new_with_capabilities(project_root, inspect_dir, config, symbol_cache, true, true)
283 }
284
285 pub fn new_with_capabilities(
286 project_root: PathBuf,
287 inspect_dir: PathBuf,
288 config: Arc<Config>,
289 symbol_cache: SharedSymbolCache,
290 inspect_writer: bool,
291 callgraph_writer: bool,
292 ) -> Self {
293 Self {
294 project_root,
295 inspect_dir,
296 config,
297 symbol_cache,
298 inspect_writer,
299 callgraph_writer,
300 }
301 }
302}
303
304#[derive(Clone)]
305pub struct WorkerCtx {
306 pub project_root: PathBuf,
307 pub inspect_dir: PathBuf,
308 pub config: Arc<Config>,
309 pub symbol_cache: SharedSymbolCache,
310}
311
312impl From<&InspectSnapshot> for WorkerCtx {
313 fn from(snapshot: &InspectSnapshot) -> Self {
314 Self {
315 project_root: snapshot.project_root.clone(),
316 inspect_dir: snapshot.inspect_dir.clone(),
317 config: Arc::clone(&snapshot.config),
318 symbol_cache: Arc::clone(&snapshot.symbol_cache),
319 }
320 }
321}
322
323#[derive(Clone)]
324pub struct InspectJob {
325 pub job_id: u64,
326 pub key: JobKey,
327 pub category: InspectCategory,
328 pub scope_files: Vec<PathBuf>,
329 pub project_root: PathBuf,
330 pub inspect_dir: PathBuf,
331 pub config: Arc<Config>,
332 pub symbol_cache: SharedSymbolCache,
333 pub inspect_writer: bool,
334 pub callgraph_writer: bool,
335 pub callgraph_snapshot: Option<Arc<CallgraphSnapshot>>,
336}
337
338impl InspectJob {
339 pub fn worker_ctx(&self) -> WorkerCtx {
340 WorkerCtx {
341 project_root: self.project_root.clone(),
342 inspect_dir: self.inspect_dir.clone(),
343 config: Arc::clone(&self.config),
344 symbol_cache: Arc::clone(&self.symbol_cache),
345 }
346 }
347
348 pub fn is_full_project_scope(&self) -> bool {
351 self.key.scope_hash.as_deref() == Some("project")
352 }
353}
354
355pub(crate) fn is_js_ts_language(language: LangId) -> bool {
356 matches!(
357 language,
358 LangId::TypeScript | LangId::Tsx | LangId::JavaScript
359 )
360}
361
362pub(crate) fn dead_code_supports_language(language: LangId) -> bool {
363 is_js_ts_language(language) || callgraph_store_dead_code_supports_language(language)
364}
365
366pub(crate) fn dead_code_skipped_language(file: &Path) -> Option<&'static str> {
367 let language = crate::parser::detect_language(file)?;
368 (!dead_code_supports_language(language)).then(|| language_name(language))
369}
370
371fn callgraph_store_dead_code_supports_language(language: LangId) -> bool {
372 let supported_by_store_liveness = matches!(
378 language,
379 LangId::Rust | LangId::Go | LangId::C | LangId::Cpp | LangId::Zig | LangId::CSharp
380 );
381 supported_by_store_liveness && !crate::calls::call_node_kinds(language).is_empty()
382}
383
384pub(crate) fn language_name(language: LangId) -> &'static str {
385 match language {
386 LangId::TypeScript => "typescript",
387 LangId::Tsx => "tsx",
388 LangId::JavaScript => "javascript",
389 LangId::Python => "python",
390 LangId::Rust => "rust",
391 LangId::Go => "go",
392 LangId::C => "c",
393 LangId::Cpp => "cpp",
394 LangId::Zig => "zig",
395 LangId::CSharp => "csharp",
396 LangId::Bash => "bash",
397 LangId::Html => "html",
398 LangId::Markdown => "markdown",
399 LangId::Yaml => "yaml",
400 LangId::Solidity => "solidity",
401 LangId::Scss => "scss",
402 LangId::Vue => "vue",
403 LangId::Json => "json",
404 LangId::Scala => "scala",
405 LangId::Java => "java",
406 LangId::Ruby => "ruby",
407 LangId::Kotlin => "kotlin",
408 LangId::Swift => "swift",
409 LangId::Php => "php",
410 LangId::Lua => "lua",
411 LangId::Perl => "perl",
412 LangId::Pascal => "pascal",
413 LangId::R => "r",
414 LangId::Groovy => "groovy",
415 LangId::ObjC => "objc",
416 }
417}
418
419#[derive(Debug, Clone, Default)]
420pub struct CallgraphSnapshot {
421 pub generated_at: Option<SystemTime>,
422 pub files: Vec<PathBuf>,
423 pub exported_symbols: Vec<CallgraphExport>,
424 pub outbound_calls: Vec<CallgraphOutboundCall>,
425 pub entry_points: BTreeSet<PathBuf>,
426 pub entry_point_symbols: BTreeMap<PathBuf, BTreeSet<String>>,
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
430pub struct CallgraphExport {
431 pub file: PathBuf,
432 pub symbol: String,
433 pub kind: String,
434 pub line: u32,
435}
436
437pub(crate) const DISPATCHED_CALLEE_SEPARATOR: char = '\u{1f}';
438pub(crate) const CALLGRAPH_PROVENANCE_TREESITTER: &str = "treesitter";
439pub(crate) const CALLGRAPH_PROVENANCE_REEXPORT: &str = "reexport";
440
441fn default_callgraph_outbound_provenance() -> String {
442 CALLGRAPH_PROVENANCE_TREESITTER.to_string()
443}
444
445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
446pub struct CallgraphOutboundCall {
447 pub caller_file: PathBuf,
448 pub caller_symbol: String,
449 pub target: String,
450 pub line: u32,
451 #[serde(default = "default_callgraph_outbound_provenance")]
452 pub provenance: String,
453}
454
455#[derive(Debug, Clone)]
456pub struct FileContribution {
457 pub category: InspectCategory,
458 pub file_path: PathBuf,
459 pub freshness: FileFreshness,
460 pub contribution: serde_json::Value,
461 pub type_ref_names: BTreeSet<String>,
462}
463
464impl FileContribution {
465 pub fn new(
466 category: InspectCategory,
467 file_path: impl Into<PathBuf>,
468 freshness: FileFreshness,
469 contribution: serde_json::Value,
470 ) -> Self {
471 let type_ref_names = type_ref_names_from_contribution(&contribution);
472 Self {
473 category,
474 file_path: file_path.into(),
475 freshness,
476 contribution,
477 type_ref_names,
478 }
479 }
480
481 pub fn with_type_ref_names<I>(mut self, type_ref_names: I) -> Self
482 where
483 I: IntoIterator<Item = String>,
484 {
485 self.type_ref_names = type_ref_names.into_iter().collect();
486 self.contribution =
487 contribution_with_type_ref_names(self.contribution, &self.type_ref_names);
488 self
489 }
490}
491
492pub(crate) fn type_ref_names_from_contribution(
493 contribution: &serde_json::Value,
494) -> BTreeSet<String> {
495 contribution
496 .get("type_ref_names")
497 .and_then(serde_json::Value::as_array)
498 .into_iter()
499 .flatten()
500 .filter_map(serde_json::Value::as_str)
501 .map(str::trim)
502 .filter(|name| !name.is_empty())
503 .map(str::to_string)
504 .collect()
505}
506
507pub(crate) fn contribution_with_type_ref_names(
508 mut contribution: serde_json::Value,
509 type_ref_names: &BTreeSet<String>,
510) -> serde_json::Value {
511 if let serde_json::Value::Object(object) = &mut contribution {
512 if type_ref_names.is_empty() {
513 object.remove("type_ref_names");
514 } else {
515 object.insert(
516 "type_ref_names".to_string(),
517 serde_json::Value::Array(
518 type_ref_names
519 .iter()
520 .map(|name| serde_json::Value::String(name.clone()))
521 .collect(),
522 ),
523 );
524 }
525 }
526 contribution
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
530#[serde(rename_all = "snake_case")]
531pub enum JobStatus {
532 Queued,
533 Running,
534 Completed,
535 Failed,
536}
537
538#[derive(Debug, Clone)]
539pub struct InspectScanSuccess {
540 pub scanned_files: Vec<PathBuf>,
541 pub contributions: Vec<FileContribution>,
542 pub aggregate: serde_json::Value,
543}
544
545#[derive(Debug, Clone)]
546pub struct InspectResult {
547 pub job_id: u64,
548 pub key: JobKey,
549 pub category: InspectCategory,
550 pub project_root: PathBuf,
551 pub inspect_dir: PathBuf,
552 pub config: Arc<Config>,
553 pub outcome: Result<InspectScanSuccess, String>,
554 pub duration: Duration,
555}
556
557impl InspectResult {
558 pub fn success(job: &InspectJob, success: InspectScanSuccess, duration: Duration) -> Self {
559 Self {
560 job_id: job.job_id,
561 key: job.key.clone(),
562 category: job.category,
563 project_root: job.project_root.clone(),
564 inspect_dir: job.inspect_dir.clone(),
565 config: Arc::clone(&job.config),
566 outcome: Ok(success),
567 duration,
568 }
569 }
570
571 pub fn failed(job: &InspectJob, message: impl Into<String>, duration: Duration) -> Self {
572 Self {
573 job_id: job.job_id,
574 key: job.key.clone(),
575 category: job.category,
576 project_root: job.project_root.clone(),
577 inspect_dir: job.inspect_dir.clone(),
578 config: Arc::clone(&job.config),
579 outcome: Err(message.into()),
580 duration,
581 }
582 }
583}
584
585#[derive(Debug, Clone, Serialize)]
586#[serde(tag = "status", rename_all = "snake_case")]
587pub enum JobOutcome {
588 Fresh {
589 payload: serde_json::Value,
590 },
591 Stale {
592 cached: Option<serde_json::Value>,
593 in_flight: bool,
594 },
595 Pending {
596 in_flight: bool,
597 },
598 Failed {
599 message: String,
600 },
601}
602
603impl JobOutcome {
604 pub fn payload(&self) -> Option<&serde_json::Value> {
605 match self {
606 JobOutcome::Fresh { payload } => Some(payload),
607 JobOutcome::Stale { cached, .. } => cached.as_ref(),
608 JobOutcome::Pending { .. } | JobOutcome::Failed { .. } => None,
609 }
610 }
611
612 pub fn is_stale(&self) -> bool {
613 matches!(self, JobOutcome::Stale { .. })
614 }
615
616 pub fn is_pending(&self) -> bool {
617 matches!(self, JobOutcome::Pending { .. })
618 }
619
620 pub fn summary_status(&self) -> Option<&'static str> {
621 match self {
622 JobOutcome::Fresh { .. } => None,
623 JobOutcome::Stale { .. } => Some("stale"),
624 JobOutcome::Pending { .. } => Some("pending"),
625 JobOutcome::Failed { .. } => Some("failed"),
626 }
627 }
628}
629
630pub(crate) fn is_test_support_file(relative_path: &str) -> bool {
641 let normalized = relative_path.replace('\\', "/");
642 normalized.split('/').any(|segment| {
643 matches!(
644 segment,
645 "fixtures"
646 | "__fixtures__"
647 | "testdata"
648 | "test-data"
649 | "__mocks__"
650 | "__snapshots__"
651 | "corpora"
652 )
653 })
654}
655
656pub(crate) fn is_test_file(relative_path: &str) -> bool {
666 let normalized = relative_path.replace('\\', "/");
667
668 if normalized
672 .split('/')
673 .any(|segment| matches!(segment, "__tests__" | "__test__" | "tests"))
674 {
675 return true;
676 }
677
678 let file = normalized.rsplit('/').next().unwrap_or(&normalized);
679 let lower = file.to_ascii_lowercase();
680
681 if lower.contains(".test.") || lower.contains(".spec.") {
683 return true;
684 }
685
686 if lower.ends_with("_test.rs")
688 || lower.ends_with("_test.go")
689 || lower.ends_with("_test.py")
690 || lower.ends_with("_test.rb")
691 || lower.ends_with("_test.exs")
692 || lower.ends_with("_spec.rb")
693 || (lower.starts_with("test_") && lower.ends_with(".py"))
694 {
695 return true;
696 }
697
698 const CAMEL_SUFFIXES: &[&str] = &[
701 "Test.java",
702 "Tests.java",
703 "Test.kt",
704 "Tests.kt",
705 "Test.cs",
706 "Tests.cs",
707 "Test.swift",
708 "Tests.swift",
709 "Test.scala",
710 "Spec.scala",
711 ];
712 CAMEL_SUFFIXES.iter().any(|suffix| file.ends_with(suffix))
713}
714
715pub(crate) fn normalize_path(path: &Path) -> PathBuf {
716 #[cfg(windows)]
722 let path = &windows_non_verbatim_path(path);
723
724 let mut result = PathBuf::new();
725 for component in path.components() {
726 match component {
727 std::path::Component::CurDir => {}
728 std::path::Component::ParentDir => {
729 if !result.pop() {
730 result.push(component);
731 }
732 }
733 other => result.push(other.as_os_str()),
734 }
735 }
736 result
737}
738
739pub(crate) fn canonicalize_normalized(path: &Path) -> PathBuf {
746 match std::fs::canonicalize(path) {
747 Ok(canonical) => normalize_path(&canonical),
748 Err(_) => normalize_path(path),
749 }
750}
751
752#[cfg(windows)]
753fn windows_non_verbatim_path(path: &Path) -> PathBuf {
754 let mut raw = path.to_string_lossy().replace('/', "\\");
755 if let Some(stripped) = raw.strip_prefix("\\\\?\\UNC\\") {
756 raw = format!("\\\\{stripped}");
757 } else if let Some(stripped) = raw.strip_prefix("\\\\?\\") {
758 raw = stripped.to_string();
759 } else if let Some(stripped) = raw.strip_prefix("\\\\??\\") {
760 raw = stripped.to_string();
761 }
762
763 if raw.as_bytes().get(1) == Some(&b':') {
764 let drive = raw.as_bytes()[0];
765 if drive.is_ascii_lowercase() {
766 raw.replace_range(0..1, &(drive as char).to_ascii_uppercase().to_string());
767 }
768 }
769
770 PathBuf::from(raw)
771}
772
773#[cfg(test)]
774mod test_support_tests {
775 use super::{is_test_file, is_test_support_file};
776
777 #[test]
778 fn is_test_file_matches_real_test_files() {
779 for p in [
781 "src/foo.test.ts",
782 "src/foo.test.tsx",
783 "src/bar.spec.js",
784 "packages/x/component.test.jsx",
785 "app/foo.test.mjs",
786 "src/comp.spec.vue",
787 ] {
788 assert!(is_test_file(p), "{p} should be a test file");
789 }
790 assert!(is_test_file("packages/x/__tests__/reading.ts"));
792 assert!(is_test_file("crates/aft/tests/integration/main.rs"));
793 assert!(is_test_file("crates/aft/src/foo_test.rs"));
795 assert!(is_test_file("pkg/handler_test.go"));
796 assert!(is_test_file("app/test_models.py"));
797 assert!(is_test_file("app/models_test.py"));
798 assert!(is_test_file("spec/user_spec.rb"));
799 assert!(is_test_file("src/main/UserServiceTest.java"));
801 assert!(is_test_file("src/FooTests.cs"));
802 assert!(is_test_file("Sources/AppTests.swift"));
803 assert!(is_test_file("packages\\x\\__tests__\\a.ts"));
805 }
806
807 #[test]
808 fn is_test_file_rejects_product_files() {
809 for p in [
810 "crates/aft/src/inspect/job.rs",
811 "packages/x/src/index.ts",
812 "src/contestant.ts", "src/greatest.ts", "src/latest.java", "src/my_attestation.py", "src/test/helper.ts", ] {
818 assert!(!is_test_file(p), "{p} must NOT be a test file");
819 }
820 }
821
822 #[test]
823 fn matches_conventional_support_dirs() {
824 assert!(is_test_support_file("crates/aft/tests/fixtures/sample.ts"));
825 assert!(is_test_support_file(
826 "packages/x/__tests__/e2e/fixtures/a.ts"
827 ));
828 assert!(is_test_support_file(
829 "benchmarks/codegraph/corpora/repo/lib.go"
830 ));
831 assert!(is_test_support_file("src/__mocks__/fs.ts"));
832 assert!(is_test_support_file("src/__snapshots__/render.snap"));
833 assert!(is_test_support_file("internal/testdata/golden.json"));
834 assert!(is_test_support_file("crates\\aft\\tests\\fixtures\\x.rs"));
836 }
837
838 #[test]
839 fn does_not_match_product_or_test_files() {
840 assert!(!is_test_support_file("crates/aft/src/inspect/job.rs"));
842 assert!(!is_test_support_file(
844 "packages/x/__tests__/reading.test.ts"
845 ));
846 assert!(!is_test_support_file(
847 "crates/aft/tests/integration/main.rs"
848 ));
849 assert!(!is_test_support_file("src/fixturesHelper.ts"));
851 assert!(!is_test_support_file("src/my_corpora_loader.rs"));
852 }
853}