Skip to main content

aptu_coder_core/languages/
mod.rs

1// SPDX-FileCopyrightText: 2026 aptu-coder contributors
2// SPDX-License-Identifier: Apache-2.0
3//! Language-specific handlers and query definitions for tree-sitter parsing.
4//!
5//! Provides query strings and extraction handlers for supported languages.
6//! Language support is controlled by Cargo `lang-*` features (by default all
7//! available language handlers are enabled): Astro, C/C++, C#, CSS, Fortran, Go,
8//! HTML, Java, JavaScript, JSON, Kotlin, Markdown, Python, Rust, TOML, TSX, TypeScript, YAML.
9
10pub mod cpp;
11pub mod csharp;
12pub mod css;
13pub mod fortran;
14pub mod go;
15pub mod html;
16pub mod java;
17pub mod javascript;
18pub mod kotlin;
19pub mod markdown;
20pub mod python;
21pub mod regex_fallback;
22pub mod rust;
23pub mod typescript;
24pub mod yaml;
25
26use tree_sitter::{Language, Node};
27
28/// Extract the source text for a node with a bounds check.
29///
30/// Returns `None` if the node's byte range falls outside `source`.
31#[must_use]
32pub fn get_node_text(node: &Node, source: &str) -> Option<String> {
33    let end = node.end_byte();
34    if end <= source.len() {
35        Some(source[node.start_byte()..end].to_string())
36    } else {
37        None
38    }
39}
40
41/// Handler to extract function name from a node.
42pub type ExtractFunctionNameHandler = fn(&Node, &str, &str) -> Option<String>;
43
44/// Handler to find method name for a receiver type.
45pub type FindMethodForReceiverHandler = fn(&Node, &str, Option<usize>) -> Option<String>;
46
47/// Handler to find receiver type for a method.
48pub type FindReceiverTypeHandler = fn(&Node, &str) -> Option<String>;
49
50/// Handler to extract inheritance information from a class node.
51pub type ExtractInheritanceHandler = fn(&Node, &str) -> Vec<String>;
52
53/// Information about a supported language for code analysis.
54pub struct LanguageInfo {
55    pub name: &'static str,
56    pub language: Language,
57    pub element_query: &'static str,
58    pub call_query: &'static str,
59    pub reference_query: Option<&'static str>,
60    pub import_query: Option<&'static str>,
61    pub impl_query: Option<&'static str>,
62    pub impl_trait_query: Option<&'static str>,
63    pub defuse_query: Option<&'static str>,
64    pub extract_function_name: Option<ExtractFunctionNameHandler>,
65    pub find_method_for_receiver: Option<FindMethodForReceiverHandler>,
66    pub find_receiver_type: Option<FindReceiverTypeHandler>,
67    pub extract_inheritance: Option<ExtractInheritanceHandler>,
68}
69
70/// Get language information by language name.
71#[allow(clippy::too_many_lines)] // exhaustive match over all supported languages; splitting harms readability
72pub fn get_language_info(lang_name: &str) -> Option<LanguageInfo> {
73    match lang_name {
74        "rust" => Some(LanguageInfo {
75            name: "rust",
76            language: tree_sitter_rust::LANGUAGE.into(),
77            element_query: rust::ELEMENT_QUERY,
78            call_query: rust::CALL_QUERY,
79            reference_query: Some(rust::REFERENCE_QUERY),
80            import_query: Some(rust::IMPORT_QUERY),
81            impl_query: Some(rust::IMPL_QUERY),
82            impl_trait_query: Some(rust::IMPL_TRAIT_QUERY),
83            defuse_query: Some(rust::DEFUSE_QUERY),
84            extract_function_name: Some(rust::extract_function_name),
85            find_method_for_receiver: Some(rust::find_method_for_receiver),
86            find_receiver_type: Some(rust::find_receiver_type),
87            extract_inheritance: Some(rust::extract_inheritance),
88        }),
89        "python" => Some(LanguageInfo {
90            name: "python",
91            language: tree_sitter_python::LANGUAGE.into(),
92            element_query: python::ELEMENT_QUERY,
93            call_query: python::CALL_QUERY,
94            reference_query: Some(python::REFERENCE_QUERY),
95            import_query: Some(python::IMPORT_QUERY),
96            impl_query: None,
97            impl_trait_query: None,
98            defuse_query: Some(python::DEFUSE_QUERY),
99            extract_function_name: None,
100            find_method_for_receiver: None,
101            find_receiver_type: None,
102            extract_inheritance: Some(python::extract_inheritance),
103        }),
104        "typescript" => Some(LanguageInfo {
105            name: "typescript",
106            language: tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
107            element_query: typescript::ELEMENT_QUERY,
108            call_query: typescript::CALL_QUERY,
109            reference_query: Some(typescript::REFERENCE_QUERY),
110            import_query: Some(typescript::IMPORT_QUERY),
111            impl_query: None,
112            impl_trait_query: None,
113            defuse_query: Some(typescript::DEFUSE_QUERY),
114            extract_function_name: None,
115            find_method_for_receiver: None,
116            find_receiver_type: None,
117            extract_inheritance: Some(typescript::extract_inheritance),
118        }),
119        "tsx" => Some(LanguageInfo {
120            name: "tsx",
121            language: tree_sitter_typescript::LANGUAGE_TSX.into(),
122            element_query: typescript::ELEMENT_QUERY,
123            call_query: typescript::CALL_QUERY,
124            reference_query: Some(typescript::REFERENCE_QUERY),
125            import_query: Some(typescript::IMPORT_QUERY),
126            impl_query: None,
127            impl_trait_query: None,
128            defuse_query: Some(typescript::DEFUSE_QUERY),
129            extract_function_name: None,
130            find_method_for_receiver: None,
131            find_receiver_type: None,
132            extract_inheritance: Some(typescript::extract_inheritance),
133        }),
134        "go" => Some(LanguageInfo {
135            name: "go",
136            language: tree_sitter_go::LANGUAGE.into(),
137            element_query: go::ELEMENT_QUERY,
138            call_query: go::CALL_QUERY,
139            reference_query: Some(go::REFERENCE_QUERY),
140            import_query: Some(go::IMPORT_QUERY),
141            impl_query: None,
142            impl_trait_query: None,
143            defuse_query: Some(go::DEFUSE_QUERY),
144            extract_function_name: Some(go::extract_function_name),
145            find_method_for_receiver: Some(go::find_method_for_receiver),
146            find_receiver_type: Some(go::find_receiver_type),
147            extract_inheritance: Some(go::extract_inheritance),
148        }),
149        "c" | "cpp" => Some(LanguageInfo {
150            name: if lang_name == "c" { "c" } else { "cpp" },
151            language: tree_sitter_cpp::LANGUAGE.into(),
152            element_query: cpp::ELEMENT_QUERY,
153            call_query: cpp::CALL_QUERY,
154            reference_query: Some(cpp::REFERENCE_QUERY),
155            import_query: Some(cpp::IMPORT_QUERY),
156            impl_query: None,
157            impl_trait_query: None,
158            defuse_query: Some(cpp::DEFUSE_QUERY),
159            extract_function_name: Some(cpp::extract_function_name),
160            find_method_for_receiver: Some(cpp::find_method_for_receiver),
161            find_receiver_type: None,
162            extract_inheritance: Some(cpp::extract_inheritance),
163        }),
164        "java" => Some(LanguageInfo {
165            name: "java",
166            language: tree_sitter_java::LANGUAGE.into(),
167            element_query: java::ELEMENT_QUERY,
168            call_query: java::CALL_QUERY,
169            reference_query: Some(java::REFERENCE_QUERY),
170            import_query: Some(java::IMPORT_QUERY),
171            impl_query: None,
172            impl_trait_query: None,
173            defuse_query: Some(java::DEFUSE_QUERY),
174            extract_function_name: Some(java::extract_function_name),
175            find_method_for_receiver: Some(java::find_method_for_receiver),
176            find_receiver_type: Some(java::find_receiver_type),
177            extract_inheritance: Some(java::extract_inheritance),
178        }),
179        "kotlin" => Some(LanguageInfo {
180            name: "kotlin",
181            language: tree_sitter_kotlin_ng::LANGUAGE.into(),
182            element_query: kotlin::ELEMENT_QUERY,
183            call_query: kotlin::CALL_QUERY,
184            reference_query: Some(kotlin::REFERENCE_QUERY),
185            import_query: Some(kotlin::IMPORT_QUERY),
186            impl_query: None,
187            impl_trait_query: None,
188            defuse_query: Some(kotlin::DEFUSE_QUERY),
189            extract_function_name: Some(kotlin::extract_function_name),
190            find_method_for_receiver: Some(kotlin::find_method_for_receiver),
191            find_receiver_type: Some(kotlin::find_receiver_type),
192            extract_inheritance: Some(kotlin::extract_inheritance),
193        }),
194        "fortran" => Some(LanguageInfo {
195            name: "fortran",
196            language: tree_sitter_fortran::LANGUAGE.into(),
197            element_query: fortran::ELEMENT_QUERY,
198            call_query: fortran::CALL_QUERY,
199            reference_query: Some(fortran::REFERENCE_QUERY),
200            import_query: Some(fortran::IMPORT_QUERY),
201            impl_query: None,
202            impl_trait_query: None,
203            defuse_query: None,
204            extract_function_name: Some(fortran::extract_function_name),
205            find_method_for_receiver: Some(fortran::find_method_for_receiver),
206            find_receiver_type: Some(fortran::find_receiver_type),
207            extract_inheritance: Some(fortran::extract_inheritance),
208        }),
209        "csharp" => Some(LanguageInfo {
210            name: "csharp",
211            language: tree_sitter_c_sharp::LANGUAGE.into(),
212            element_query: csharp::ELEMENT_QUERY,
213            call_query: csharp::CALL_QUERY,
214            reference_query: Some(csharp::REFERENCE_QUERY),
215            import_query: Some(csharp::IMPORT_QUERY),
216            impl_query: None,
217            impl_trait_query: None,
218            defuse_query: Some(csharp::DEFUSE_QUERY),
219            extract_function_name: Some(csharp::extract_function_name),
220            find_method_for_receiver: Some(csharp::find_method_for_receiver),
221            find_receiver_type: Some(csharp::find_receiver_type),
222            extract_inheritance: Some(csharp::extract_inheritance),
223        }),
224        "javascript" => Some(LanguageInfo {
225            name: "javascript",
226            language: tree_sitter_javascript::LANGUAGE.into(),
227            element_query: javascript::ELEMENT_QUERY,
228            call_query: javascript::CALL_QUERY,
229            reference_query: None,
230            import_query: Some(javascript::IMPORT_QUERY),
231            impl_query: None,
232            impl_trait_query: None,
233            defuse_query: Some(javascript::DEFUSE_QUERY),
234            extract_function_name: Some(javascript::extract_function_name),
235            find_method_for_receiver: Some(javascript::find_method_for_receiver),
236            find_receiver_type: Some(javascript::find_receiver_type),
237            extract_inheritance: Some(javascript::extract_inheritance),
238        }),
239        // HTML is a reserved feature stub. `tree-sitter-html` 0.23.x is incompatible with the
240        // tree-sitter 0.26 API used by this crate; full HTML support is blocked on the
241        // tree-sitter-html ^0.25 release. Until then, analysis of `.html`/`.htm` files returns
242        // `None` here, which causes `analyze_file` to emit an INVALID_PARAMS error with the
243        // message "unsupported language: html". This is intentional: the extension is registered
244        // so that the file-type is recognised and a clear error surfaces rather than silently
245        // skipping the file.
246        // TODO: implement once tree-sitter-html ^0.25 ships.
247        //       Track releases: https://github.com/tree-sitter/tree-sitter-html/releases
248        "html" => None,
249        "markdown" => Some(LanguageInfo {
250            name: "markdown",
251            language: tree_sitter_md::LANGUAGE.into(),
252            element_query: markdown::ELEMENT_QUERY,
253            call_query: markdown::CALL_QUERY,
254            reference_query: None,
255            import_query: None,
256            impl_query: None,
257            impl_trait_query: None,
258            defuse_query: None,
259            extract_function_name: None,
260            find_method_for_receiver: None,
261            find_receiver_type: None,
262            extract_inheritance: None,
263        }),
264        "css" => Some(LanguageInfo {
265            name: "css",
266            language: tree_sitter_css::LANGUAGE.into(),
267            element_query: css::ELEMENT_QUERY,
268            call_query: css::CALL_QUERY,
269            reference_query: None,
270            import_query: Some(css::IMPORT_QUERY),
271            impl_query: None,
272            impl_trait_query: None,
273            defuse_query: None,
274            extract_function_name: None,
275            find_method_for_receiver: None,
276            find_receiver_type: None,
277            extract_inheritance: None,
278        }),
279        "yaml" => Some(LanguageInfo {
280            name: "yaml",
281            language: tree_sitter_yaml::LANGUAGE.into(),
282            element_query: yaml::ELEMENT_QUERY,
283            call_query: yaml::CALL_QUERY,
284            reference_query: None,
285            import_query: None,
286            impl_query: None,
287            impl_trait_query: None,
288            defuse_query: None,
289            extract_function_name: None,
290            find_method_for_receiver: None,
291            find_receiver_type: None,
292            extract_inheritance: None,
293        }),
294        _ => None,
295    }
296}
297
298/// Get the tree-sitter Language object for a given language name.
299///
300/// Returns `None` if the language is not supported or not compiled in.
301#[must_use]
302pub fn get_ts_language(lang_name: &str) -> Option<Language> {
303    match lang_name {
304        "rust" => Some(tree_sitter_rust::LANGUAGE.into()),
305        "python" => Some(tree_sitter_python::LANGUAGE.into()),
306        "typescript" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
307        "tsx" => Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
308        "go" => Some(tree_sitter_go::LANGUAGE.into()),
309        "c" | "cpp" => Some(tree_sitter_cpp::LANGUAGE.into()),
310        "java" => Some(tree_sitter_java::LANGUAGE.into()),
311        "kotlin" => Some(tree_sitter_kotlin_ng::LANGUAGE.into()),
312        "fortran" => Some(tree_sitter_fortran::LANGUAGE.into()),
313        "csharp" => Some(tree_sitter_c_sharp::LANGUAGE.into()),
314        "javascript" => Some(tree_sitter_javascript::LANGUAGE.into()),
315        "css" => Some(tree_sitter_css::LANGUAGE.into()),
316        "yaml" => Some(tree_sitter_yaml::LANGUAGE.into()),
317        _ => None,
318    }
319}
320
321/// Attempt regex-based extraction for formats without a tree-sitter grammar.
322///
323/// Returns `Some(SemanticAnalysis)` for CSS, YAML, JSON, TOML, and Astro;
324/// `None` for all other language identifiers (caller should treat as unsupported).
325#[must_use]
326pub fn try_regex_fallback(source: &str, language: &str) -> Option<crate::types::SemanticAnalysis> {
327    match language {
328        "css" => Some(regex_fallback::extract_css(source)),
329        "yaml" => Some(regex_fallback::extract_yaml(source)),
330        "json" => Some(regex_fallback::extract_json(source)),
331        "toml" => Some(regex_fallback::extract_toml(source)),
332        "astro" => Some(regex_fallback::extract_astro(source)),
333        _ => None,
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_get_language_info_known() {
343        // Happy path: known languages return Some
344        assert!(
345            get_language_info("rust").is_some(),
346            "expected Some for 'rust'"
347        );
348        assert!(get_language_info("go").is_some(), "expected Some for 'go'");
349        assert!(
350            get_language_info("python").is_some(),
351            "expected Some for 'python'"
352        );
353    }
354
355    #[test]
356    fn test_get_language_info_unknown() {
357        // Edge case: unknown language returns None
358        assert!(
359            get_language_info("cobol").is_none(),
360            "expected None for 'cobol'"
361        );
362    }
363
364    #[test]
365    fn test_get_ts_language_known() {
366        // Happy path: known language returns Some
367        assert!(
368            get_ts_language("rust").is_some(),
369            "expected Some for 'rust'"
370        );
371    }
372
373    #[test]
374    fn test_get_ts_language_unknown() {
375        // Edge case: unknown language returns None
376        assert!(
377            get_ts_language("cobol").is_none(),
378            "expected None for 'cobol'"
379        );
380    }
381}