Skip to main content

exspec_core/
extractor.rs

1use crate::rules::RuleId;
2
3#[derive(Debug, Clone, Default)]
4pub struct TestAnalysis {
5    pub assertion_count: usize,
6    pub mock_count: usize,
7    pub mock_classes: Vec<String>,
8    pub line_count: usize,
9    pub how_not_what_count: usize,
10    pub fixture_count: usize,
11    pub has_wait: bool,
12    pub assertion_message_count: usize,
13    pub duplicate_literal_count: usize,
14    pub suppressed_rules: Vec<RuleId>,
15}
16
17#[derive(Debug, Clone)]
18pub struct TestFunction {
19    pub name: String,
20    pub file: String,
21    pub line: usize,
22    pub end_line: usize,
23    pub analysis: TestAnalysis,
24}
25
26/// File-level analysis result for rules that operate at file scope (T004-T008).
27///
28/// Language extractors MUST override `extract_file_analysis()` to provide
29/// accurate `has_pbt_import`, `has_contract_import`, `has_error_test`,
30/// and `parameterized_count`.
31/// The default impl returns false/0 for these fields.
32#[derive(Debug, Clone)]
33pub struct FileAnalysis {
34    pub file: String,
35    pub functions: Vec<TestFunction>,
36    pub has_pbt_import: bool,
37    pub has_contract_import: bool,
38    pub has_error_test: bool,
39    pub has_relational_assertion: bool,
40    pub parameterized_count: usize,
41}
42
43pub trait LanguageExtractor {
44    fn extract_test_functions(&self, source: &str, file_path: &str) -> Vec<TestFunction>;
45
46    /// Extract file-level analysis including imports and parameterized test counts.
47    /// Default impl delegates to `extract_test_functions` with file-level fields as false/0.
48    /// Language extractors MUST override this to provide accurate detection.
49    fn extract_file_analysis(&self, source: &str, file_path: &str) -> FileAnalysis {
50        let functions = self.extract_test_functions(source, file_path);
51        FileAnalysis {
52            file: file_path.to_string(),
53            functions,
54            has_pbt_import: false,
55            has_contract_import: false,
56            has_error_test: false,
57            has_relational_assertion: false,
58            parameterized_count: 0,
59        }
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn test_analysis_default_all_zero_or_empty() {
69        let analysis = TestAnalysis::default();
70        assert_eq!(analysis.assertion_count, 0);
71        assert_eq!(analysis.mock_count, 0);
72        assert!(analysis.mock_classes.is_empty());
73        assert_eq!(analysis.line_count, 0);
74        assert_eq!(analysis.how_not_what_count, 0);
75        assert_eq!(analysis.fixture_count, 0);
76        assert!(!analysis.has_wait);
77        assert_eq!(analysis.assertion_message_count, 0);
78        assert_eq!(analysis.duplicate_literal_count, 0);
79        assert!(analysis.suppressed_rules.is_empty());
80    }
81
82    #[test]
83    fn file_analysis_fields_accessible() {
84        let fa = FileAnalysis {
85            file: "test.py".to_string(),
86            functions: vec![],
87            has_pbt_import: true,
88            has_contract_import: false,
89            has_error_test: true,
90            has_relational_assertion: false,
91            parameterized_count: 3,
92        };
93        assert_eq!(fa.file, "test.py");
94        assert!(fa.functions.is_empty());
95        assert!(fa.has_pbt_import);
96        assert!(!fa.has_contract_import);
97        assert!(fa.has_error_test);
98        assert!(!fa.has_relational_assertion);
99        assert_eq!(fa.parameterized_count, 3);
100    }
101
102    struct DummyExtractor;
103    impl LanguageExtractor for DummyExtractor {
104        fn extract_test_functions(&self, _source: &str, file_path: &str) -> Vec<TestFunction> {
105            vec![TestFunction {
106                name: "test_dummy".to_string(),
107                file: file_path.to_string(),
108                line: 1,
109                end_line: 3,
110                analysis: TestAnalysis::default(),
111            }]
112        }
113    }
114
115    #[test]
116    fn default_extract_file_analysis_delegates_to_extract_test_functions() {
117        let extractor = DummyExtractor;
118        let fa = extractor.extract_file_analysis("x = 1", "test.py");
119        assert_eq!(fa.functions.len(), 1);
120        assert!(!fa.has_pbt_import);
121        assert!(!fa.has_contract_import);
122        assert!(!fa.has_error_test);
123        assert!(!fa.has_relational_assertion);
124        assert_eq!(fa.parameterized_count, 0);
125    }
126}