Skip to main content

thread_services/
conversion.rs

1// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
2// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5//! # Conversion Utilities
6//!
7//! Utilities for converting between service layer types and ast-engine types.
8//! These functions bridge the ast-grep functionality with the service layer
9//! abstractions while preserving all ast-grep power.
10
11use crate::types::{CodeMatch, ParsedDocument, Range, SymbolInfo, SymbolKind, Visibility};
12use std::path::PathBuf;
13
14#[cfg(feature = "matching")]
15use crate::error::ServiceResult;
16#[cfg(feature = "matching")]
17use crate::types::{CallInfo, DocumentMetadata, ImportInfo, ImportKind};
18#[cfg(feature = "matching")]
19use thread_utilities::RapidMap;
20
21cfg_if::cfg_if!(
22    if #[cfg(feature = "ast-grep-backend")] {
23        use thread_ast_engine::{Doc, Root, Node, NodeMatch, Position};
24        use thread_language::SupportLang;
25    } else  {
26        use crate::types::{Doc, Root, NodeMatch, Position, SupportLang};
27    }
28);
29
30/// Convert ast-grep NodeMatch to service layer CodeMatch
31///
32/// This preserves all ast-grep functionality while adding service layer context.
33pub fn node_match_to_code_match<'tree, D: Doc>(
34    node_match: NodeMatch<'tree, D>,
35) -> CodeMatch<'tree, D> {
36    CodeMatch::new(node_match)
37}
38
39/// Create ParsedDocument from ast-grep Root
40///
41/// This is the core conversion that bridges file-level ast-grep to codebase-level intelligence.
42pub fn root_to_parsed_document<D: Doc>(
43    ast_root: Root<D>,
44    file_path: PathBuf,
45    language: SupportLang,
46    content_fingerprint: recoco_utils::fingerprint::Fingerprint,
47) -> ParsedDocument<D> {
48    ParsedDocument::new(ast_root, file_path, language, content_fingerprint)
49}
50
51/// Extract basic metadata from a parsed document
52///
53/// This function demonstrates how to build codebase-level intelligence
54/// on top of ast-grep's file-level analysis capabilities.
55#[cfg(feature = "matching")]
56pub fn extract_basic_metadata<D: Doc>(
57    document: &ParsedDocument<D>,
58) -> ServiceResult<DocumentMetadata> {
59    let mut metadata = DocumentMetadata::default();
60    let root = document.ast_grep_root();
61    let root_node = root.root();
62
63    // Extract function definitions
64    if let Ok(function_matches) = extract_functions(&root_node) {
65        for (name, info) in function_matches {
66            metadata.defined_symbols.insert(name, info);
67        }
68    }
69
70    // Extract import statements
71    if let Ok(imports) = extract_imports(&root_node, &document.language) {
72        for (name, info) in imports {
73            metadata.imported_symbols.insert(name, info);
74        }
75    }
76
77    // Extract function calls
78    if let Ok(calls) = extract_function_calls(&root_node) {
79        metadata.function_calls = calls;
80    }
81
82    Ok(metadata)
83}
84
85/// Extract function definitions using ast-grep patterns
86#[cfg(feature = "matching")]
87fn extract_functions<D: Doc>(root_node: &Node<D>) -> ServiceResult<RapidMap<String, SymbolInfo>> {
88    let mut functions = thread_utilities::get_map();
89
90    // Try different function patterns based on common languages
91    let patterns = [
92        "fn $NAME($$$PARAMS) { $$$BODY }",       // Rust
93        "function $NAME($$$PARAMS) { $$$BODY }", // JavaScript
94        "def $NAME($$$PARAMS): $$$BODY",         // Python
95        "func $NAME($$$PARAMS) { $$$BODY }",     // Go
96    ];
97
98    for pattern in &patterns {
99        for node_match in root_node.find_all(pattern) {
100            if let Some(name_node) = node_match.get_env().get_match("NAME") {
101                let function_name = name_node.text().to_string();
102                let position = name_node.start_pos();
103
104                let symbol_info = SymbolInfo {
105                    name: function_name.clone(),
106                    kind: SymbolKind::Function,
107                    position,
108                    scope: "global".to_string(),    // Simplified for now
109                    visibility: Visibility::Public, // Simplified for now
110                };
111
112                functions.insert(function_name, symbol_info);
113            }
114        }
115    }
116
117    Ok(functions)
118}
119
120/// Extract import statements using language-specific patterns
121#[cfg(feature = "matching")]
122fn extract_imports<D: Doc>(
123    root_node: &Node<D>,
124    language: &SupportLang,
125) -> ServiceResult<RapidMap<String, ImportInfo>> {
126    let mut imports = thread_utilities::get_map();
127
128    let patterns = match language {
129        SupportLang::Rust => vec!["use $PATH;", "use $PATH::$ITEM;", "use $PATH::{$$$ITEMS};"],
130        SupportLang::JavaScript | SupportLang::TypeScript => vec![
131            "import $ITEM from '$PATH';",
132            "import { $$$ITEMS } from '$PATH';",
133            "import * as $ALIAS from '$PATH';",
134        ],
135        SupportLang::Python => vec![
136            "import $MODULE",
137            "from $MODULE import $ITEM",
138            "from $MODULE import $$$ITEMS",
139        ],
140        _ => vec![], // Add more languages as needed
141    };
142
143    for pattern in patterns {
144        for node_match in root_node.find_all(pattern) {
145            if let (Some(path_node), Some(item_node)) = (
146                node_match
147                    .get_env()
148                    .get_match("PATH")
149                    .or_else(|| node_match.get_env().get_match("MODULE")),
150                node_match
151                    .get_env()
152                    .get_match("ITEM")
153                    .or_else(|| node_match.get_env().get_match("PATH")),
154            ) {
155                let import_info = ImportInfo {
156                    symbol_name: item_node.text().to_string(),
157                    source_path: path_node.text().to_string(),
158                    import_kind: ImportKind::Named, // Simplified
159                    position: item_node.start_pos(),
160                };
161
162                imports.insert(item_node.text().to_string(), import_info);
163            }
164        }
165    }
166
167    Ok(imports)
168}
169
170/// Extract function calls using ast-grep patterns
171#[cfg(feature = "matching")]
172fn extract_function_calls<D: Doc>(root_node: &Node<D>) -> ServiceResult<Vec<CallInfo>> {
173    let mut calls = Vec::new();
174
175    // Common function call patterns
176    let patterns = [
177        "$FUNC($$$ARGS)",        // Most languages
178        "$OBJ.$METHOD($$$ARGS)", // Method calls
179    ];
180
181    for pattern in &patterns {
182        for node_match in root_node.find_all(pattern) {
183            if let Some(func_node) = node_match
184                .get_env()
185                .get_match("FUNC")
186                .or_else(|| node_match.get_env().get_match("METHOD"))
187            {
188                let call_info = CallInfo {
189                    function_name: func_node.text().to_string(),
190                    position: func_node.start_pos(),
191                    arguments_count: count_arguments(&node_match),
192                    is_resolved: false, // Would need cross-file analysis
193                    target_file: None,  // Would need cross-file analysis
194                };
195
196                calls.push(call_info);
197            }
198        }
199    }
200
201    Ok(calls)
202}
203
204/// Count arguments in a function call
205#[cfg(feature = "matching")]
206fn count_arguments<D: Doc>(node_match: &NodeMatch<D>) -> usize {
207    if let Some(args_node) = node_match.get_env().get_match("ARGS") {
208        // This is a simplified count - would need language-specific parsing
209        args_node
210            .text()
211            .split(',')
212            .filter(|s| !s.trim().is_empty())
213            .count()
214    } else {
215        0
216    }
217}
218
219/// Convert ast-grep Position to service layer Range
220pub fn position_to_range(start: Position, end: Position) -> Range {
221    Range::from_ast_positions(start, end)
222}
223
224/// Helper for creating SymbolInfo with common defaults
225pub fn create_symbol_info(name: String, kind: SymbolKind, position: Position) -> SymbolInfo {
226    SymbolInfo {
227        name,
228        kind,
229        position,
230        scope: "unknown".to_string(),
231        visibility: Visibility::Public,
232    }
233}
234
235/// Compute content fingerprint for deduplication using blake3
236///
237/// This uses ReCoco's Fingerprinter which provides:
238/// - 10-100x faster hashing than SHA256 via blake3
239/// - 16-byte compact fingerprint (vs 32-byte SHA256)
240/// - Automatic integration with ReCoco's memoization system
241/// - Type-safe content-addressed caching
242pub fn compute_content_fingerprint(content: &str) -> recoco_utils::fingerprint::Fingerprint {
243    let mut fp = recoco_utils::fingerprint::Fingerprinter::default();
244    // Note: write() can fail for serialization, but with &str it won't fail
245    fp.write(content)
246        .expect("fingerprinting string should not fail");
247    fp.into_fingerprint()
248}
249
250// Conversion functions for common patterns
251
252/// Convert common node kinds to SymbolKind
253pub fn node_kind_to_symbol_kind(node_kind: &str) -> SymbolKind {
254    match node_kind {
255        "function_declaration" | "function_definition" => SymbolKind::Function,
256        "class_declaration" | "class_definition" => SymbolKind::Class,
257        "interface_declaration" => SymbolKind::Interface,
258        "variable_declaration" | "let_declaration" => SymbolKind::Variable,
259        "const_declaration" | "constant" => SymbolKind::Constant,
260        "type_declaration" | "type_definition" => SymbolKind::Type,
261        "module_declaration" => SymbolKind::Module,
262        "namespace_declaration" => SymbolKind::Namespace,
263        "enum_declaration" => SymbolKind::Enum,
264        "field_declaration" => SymbolKind::Field,
265        "property_declaration" => SymbolKind::Property,
266        "method_declaration" | "method_definition" => SymbolKind::Method,
267        "constructor_declaration" => SymbolKind::Constructor,
268        _ => SymbolKind::Other(node_kind.to_string()),
269    }
270}
271
272/// Convert visibility modifiers to Visibility enum
273pub fn modifier_to_visibility(modifier: &str) -> Visibility {
274    match modifier {
275        "pub" | "public" => Visibility::Public,
276        "priv" | "private" => Visibility::Private,
277        "protected" => Visibility::Protected,
278        "internal" => Visibility::Internal,
279        "package" => Visibility::Package,
280        _ => Visibility::Other(modifier.to_string()),
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_compute_content_fingerprint() {
290        let content = "fn main() {}";
291        let fp1 = compute_content_fingerprint(content);
292        let fp2 = compute_content_fingerprint(content);
293        assert_eq!(fp1, fp2, "Same content should produce same fingerprint");
294
295        let different_content = "fn test() {}";
296        let fp3 = compute_content_fingerprint(different_content);
297        assert_ne!(
298            fp1, fp3,
299            "Different content should produce different fingerprint"
300        );
301    }
302
303    #[test]
304    fn test_node_kind_to_symbol_kind() {
305        assert_eq!(
306            node_kind_to_symbol_kind("function_declaration"),
307            SymbolKind::Function
308        );
309        assert_eq!(
310            node_kind_to_symbol_kind("class_declaration"),
311            SymbolKind::Class
312        );
313        assert_eq!(
314            node_kind_to_symbol_kind("unknown"),
315            SymbolKind::Other("unknown".to_string())
316        );
317    }
318
319    #[test]
320    fn test_modifier_to_visibility() {
321        assert_eq!(modifier_to_visibility("pub"), Visibility::Public);
322        assert_eq!(modifier_to_visibility("private"), Visibility::Private);
323        assert_eq!(modifier_to_visibility("protected"), Visibility::Protected);
324    }
325
326    #[test]
327    fn test_create_symbol_info() {
328        let pos = Position::new(1, 0, 10);
329        let info = create_symbol_info("test_function".to_string(), SymbolKind::Function, pos);
330
331        assert_eq!(info.name, "test_function");
332        assert_eq!(info.kind, SymbolKind::Function);
333        assert_eq!(info.position, pos);
334    }
335}