Skip to main content

testgap_core/
language_registry.rs

1use crate::types::Language;
2use std::path::Path;
3
4/// Returns the tree-sitter language for parsing.
5pub fn get_language(lang: Language) -> tree_sitter::Language {
6    match lang {
7        Language::Rust => tree_sitter_rust::LANGUAGE.into(),
8        Language::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
9        Language::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
10        Language::Python => tree_sitter_python::LANGUAGE.into(),
11        Language::Go => tree_sitter_go::LANGUAGE.into(),
12    }
13}
14
15/// Returns the tree-sitter query for extracting function definitions.
16pub fn function_query(lang: Language) -> &'static str {
17    match lang {
18        Language::Rust => {
19            r#"
20            (function_item
21                name: (identifier) @name
22            ) @function
23            "#
24        }
25        Language::JavaScript => {
26            r#"
27            (function_declaration
28                name: (identifier) @name
29            ) @function
30
31            (export_statement
32                declaration: (function_declaration
33                    name: (identifier) @name
34                ) @function
35            )
36
37            (lexical_declaration
38                (variable_declarator
39                    name: (identifier) @name
40                    value: (arrow_function) @function
41                )
42            )
43
44            (variable_declaration
45                (variable_declarator
46                    name: (identifier) @name
47                    value: (arrow_function) @function
48                )
49            )
50            "#
51        }
52        Language::TypeScript => {
53            // TypeScript uses the same patterns as JavaScript plus type annotations
54            r#"
55            (function_declaration
56                name: (identifier) @name
57            ) @function
58
59            (export_statement
60                declaration: (function_declaration
61                    name: (identifier) @name
62                ) @function
63            )
64
65            (lexical_declaration
66                (variable_declarator
67                    name: (identifier) @name
68                    value: (arrow_function) @function
69                )
70            )
71            "#
72        }
73        Language::Python => {
74            r#"
75            (function_definition
76                name: (identifier) @name
77            ) @function
78            "#
79        }
80        Language::Go => {
81            r#"
82            (function_declaration
83                name: (identifier) @name
84            ) @function
85
86            (method_declaration
87                name: (field_identifier) @name
88            ) @function
89            "#
90        }
91    }
92}
93
94/// Check if a file path is a test file based on naming conventions.
95pub fn is_test_file(
96    path: &Path,
97    test_dirs: &[String],
98    suffixes: &[String],
99    prefixes: &[String],
100) -> bool {
101    let path_str = path.to_string_lossy();
102
103    // Check if the file is under a test directory
104    for dir in test_dirs {
105        if path_str.contains(&format!("/{dir}/")) || path_str.contains(&format!("\\{dir}\\")) {
106            return true;
107        }
108    }
109
110    // Check file name patterns
111    if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
112        // Handle double extensions like .test.ts, .spec.ts
113        let full_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
114
115        for suffix in suffixes {
116            if stem.ends_with(suffix) || full_name.contains(&format!("{suffix}.")) {
117                return true;
118            }
119        }
120        for prefix in prefixes {
121            if stem.starts_with(prefix) {
122                return true;
123            }
124        }
125    }
126
127    // Rust-specific: check for inline #[cfg(test)] modules (handled at parse time)
128    false
129}
130
131/// Detect the language from a file path based on its extension.
132pub fn detect_language(path: &Path) -> Option<Language> {
133    path.extension()
134        .and_then(|ext| ext.to_str())
135        .and_then(Language::from_extension)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::path::PathBuf;
142
143    // Default test patterns (matching config defaults)
144    fn test_dirs() -> Vec<String> {
145        vec![
146            "tests".into(),
147            "test".into(),
148            "__tests__".into(),
149            "spec".into(),
150        ]
151    }
152
153    fn test_suffixes() -> Vec<String> {
154        vec![
155            "_test".into(),
156            ".test".into(),
157            ".spec".into(),
158            "_spec".into(),
159        ]
160    }
161
162    fn test_prefixes() -> Vec<String> {
163        vec!["test_".into()]
164    }
165
166    // ── detect_language ─────────────────────────────────────────────
167
168    #[test]
169    fn detect_language_rust() {
170        assert_eq!(
171            detect_language(Path::new("src/lib.rs")),
172            Some(Language::Rust)
173        );
174    }
175
176    #[test]
177    fn detect_language_javascript() {
178        assert_eq!(
179            detect_language(Path::new("index.js")),
180            Some(Language::JavaScript)
181        );
182    }
183
184    #[test]
185    fn detect_language_jsx() {
186        assert_eq!(
187            detect_language(Path::new("App.jsx")),
188            Some(Language::JavaScript)
189        );
190    }
191
192    #[test]
193    fn detect_language_typescript() {
194        assert_eq!(
195            detect_language(Path::new("main.ts")),
196            Some(Language::TypeScript)
197        );
198    }
199
200    #[test]
201    fn detect_language_tsx() {
202        assert_eq!(
203            detect_language(Path::new("App.tsx")),
204            Some(Language::TypeScript)
205        );
206    }
207
208    #[test]
209    fn detect_language_python() {
210        assert_eq!(
211            detect_language(Path::new("main.py")),
212            Some(Language::Python)
213        );
214    }
215
216    #[test]
217    fn detect_language_go() {
218        assert_eq!(detect_language(Path::new("main.go")), Some(Language::Go));
219    }
220
221    #[test]
222    fn detect_language_unsupported_returns_none() {
223        assert_eq!(detect_language(Path::new("README.txt")), None);
224        assert_eq!(detect_language(Path::new("docs.md")), None);
225    }
226
227    #[test]
228    fn detect_language_no_extension_returns_none() {
229        assert_eq!(detect_language(Path::new("Makefile")), None);
230    }
231
232    // ── is_test_file: true cases ────────────────────────────────────
233
234    #[test]
235    fn is_test_file_in_tests_dir() {
236        let path = PathBuf::from("project/tests/foo.rs");
237        assert!(is_test_file(
238            &path,
239            &test_dirs(),
240            &test_suffixes(),
241            &test_prefixes()
242        ));
243    }
244
245    #[test]
246    fn is_test_file_in_test_dir() {
247        let path = PathBuf::from("project/test/helper.js");
248        assert!(is_test_file(
249            &path,
250            &test_dirs(),
251            &test_suffixes(),
252            &test_prefixes()
253        ));
254    }
255
256    #[test]
257    fn is_test_file_in_dunder_tests_dir() {
258        let path = PathBuf::from("src/__tests__/App.test.tsx");
259        assert!(is_test_file(
260            &path,
261            &test_dirs(),
262            &test_suffixes(),
263            &test_prefixes()
264        ));
265    }
266
267    #[test]
268    fn is_test_file_in_spec_dir() {
269        let path = PathBuf::from("project/spec/helper_spec.rb");
270        assert!(is_test_file(
271            &path,
272            &test_dirs(),
273            &test_suffixes(),
274            &test_prefixes()
275        ));
276    }
277
278    #[test]
279    fn is_test_file_with_test_suffix() {
280        let path = PathBuf::from("src/foo_test.rs");
281        assert!(is_test_file(
282            &path,
283            &test_dirs(),
284            &test_suffixes(),
285            &test_prefixes()
286        ));
287    }
288
289    #[test]
290    fn is_test_file_with_dot_test_suffix() {
291        let path = PathBuf::from("src/foo.test.ts");
292        assert!(is_test_file(
293            &path,
294            &test_dirs(),
295            &test_suffixes(),
296            &test_prefixes()
297        ));
298    }
299
300    #[test]
301    fn is_test_file_with_spec_suffix() {
302        let path = PathBuf::from("src/foo.spec.js");
303        assert!(is_test_file(
304            &path,
305            &test_dirs(),
306            &test_suffixes(),
307            &test_prefixes()
308        ));
309    }
310
311    #[test]
312    fn is_test_file_with_test_prefix() {
313        let path = PathBuf::from("src/test_foo.py");
314        assert!(is_test_file(
315            &path,
316            &test_dirs(),
317            &test_suffixes(),
318            &test_prefixes()
319        ));
320    }
321
322    // ── is_test_file: false cases ───────────────────────────────────
323
324    #[test]
325    fn is_test_file_regular_source_rs() {
326        let path = PathBuf::from("src/lib.rs");
327        assert!(!is_test_file(
328            &path,
329            &test_dirs(),
330            &test_suffixes(),
331            &test_prefixes()
332        ));
333    }
334
335    #[test]
336    fn is_test_file_regular_source_go() {
337        let path = PathBuf::from("cmd/main.go");
338        assert!(!is_test_file(
339            &path,
340            &test_dirs(),
341            &test_suffixes(),
342            &test_prefixes()
343        ));
344    }
345
346    #[test]
347    fn is_test_file_regular_source_py() {
348        let path = PathBuf::from("src/utils.py");
349        assert!(!is_test_file(
350            &path,
351            &test_dirs(),
352            &test_suffixes(),
353            &test_prefixes()
354        ));
355    }
356}