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