sphinx_ultra/domains/
python.rs

1use crate::domains::{
2    CrossReference, DomainObject, DomainValidator, ReferenceType, ReferenceValidationResult,
3};
4use crate::error::BuildError;
5/// Python Domain Implementation
6///
7/// Handles Python-specific objects and references like :func:, :class:, :mod:, etc.
8use std::collections::HashMap;
9
10/// Python domain validator for Python-specific references
11pub struct PythonDomain {
12    /// Objects registered in this domain
13    objects: HashMap<String, DomainObject>,
14}
15
16impl Default for PythonDomain {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl PythonDomain {
23    /// Create a new Python domain
24    pub fn new() -> Self {
25        Self {
26            objects: HashMap::new(),
27        }
28    }
29
30    /// Register a Python function
31    pub fn register_function(
32        &mut self,
33        name: String,
34        qualified_name: String,
35        signature: Option<String>,
36        docstring: Option<String>,
37        location: crate::domains::ReferenceLocation,
38    ) -> Result<(), BuildError> {
39        let object = DomainObject {
40            id: format!("py:func:{}", qualified_name),
41            name: name.clone(),
42            object_type: "function".to_string(),
43            domain: "python".to_string(),
44            definition_location: location,
45            qualified_name: qualified_name.clone(),
46            metadata: HashMap::new(),
47            signature,
48            docstring,
49        };
50
51        self.register_object(object)
52    }
53
54    /// Register a Python class
55    pub fn register_class(
56        &mut self,
57        name: String,
58        qualified_name: String,
59        docstring: Option<String>,
60        location: crate::domains::ReferenceLocation,
61    ) -> Result<(), BuildError> {
62        let object = DomainObject {
63            id: format!("py:class:{}", qualified_name),
64            name: name.clone(),
65            object_type: "class".to_string(),
66            domain: "python".to_string(),
67            definition_location: location,
68            qualified_name: qualified_name.clone(),
69            metadata: HashMap::new(),
70            signature: None,
71            docstring,
72        };
73
74        self.register_object(object)
75    }
76
77    /// Register a Python module
78    pub fn register_module(
79        &mut self,
80        name: String,
81        qualified_name: String,
82        docstring: Option<String>,
83        location: crate::domains::ReferenceLocation,
84    ) -> Result<(), BuildError> {
85        let object = DomainObject {
86            id: format!("py:mod:{}", qualified_name),
87            name: name.clone(),
88            object_type: "module".to_string(),
89            domain: "python".to_string(),
90            definition_location: location,
91            qualified_name: qualified_name.clone(),
92            metadata: HashMap::new(),
93            signature: None,
94            docstring,
95        };
96
97        self.register_object(object)
98    }
99
100    /// Find suggestions for a broken reference
101    fn find_suggestions(&self, target: &str) -> Vec<String> {
102        let mut suggestions = Vec::new();
103
104        // Look for similar names
105        for obj in self.objects.values() {
106            // Exact match on simple name or fuzzy match or qualified name match
107            if obj.name == target
108                || obj.name.contains(target)
109                || target.contains(&obj.name)
110                || obj.qualified_name.contains(target)
111            {
112                suggestions.push(obj.qualified_name.clone());
113            }
114        }
115
116        suggestions.sort();
117        suggestions.dedup();
118        suggestions.truncate(5); // Limit to 5 suggestions
119
120        suggestions
121    }
122}
123
124impl DomainValidator for PythonDomain {
125    fn domain_name(&self) -> &str {
126        "python"
127    }
128
129    fn supported_reference_types(&self) -> Vec<ReferenceType> {
130        vec![
131            ReferenceType::Function,
132            ReferenceType::Class,
133            ReferenceType::Module,
134            ReferenceType::Method,
135            ReferenceType::Attribute,
136            ReferenceType::Data,
137            ReferenceType::Exception,
138        ]
139    }
140
141    fn register_object(&mut self, object: DomainObject) -> Result<(), BuildError> {
142        let key = object.qualified_name.clone();
143        self.objects.insert(key, object);
144        Ok(())
145    }
146
147    fn resolve_reference(&self, reference: &CrossReference) -> Option<DomainObject> {
148        // Try exact qualified name match
149        if let Some(obj) = self.objects.get(&reference.target) {
150            return Some(obj.clone());
151        }
152
153        // Try simple name match
154        for obj in self.objects.values() {
155            if obj.name == reference.target {
156                return Some(obj.clone());
157            }
158        }
159
160        None
161    }
162
163    fn validate_reference(&self, reference: &CrossReference) -> ReferenceValidationResult {
164        if let Some(target_object) = self.resolve_reference(reference) {
165            ReferenceValidationResult {
166                reference: reference.clone(),
167                is_valid: true,
168                target_object: Some(target_object),
169                error_message: None,
170                suggestions: Vec::new(),
171            }
172        } else {
173            let suggestions = self.find_suggestions(&reference.target);
174            let error_message = if suggestions.is_empty() {
175                format!("Python object '{}' not found", reference.target)
176            } else {
177                format!(
178                    "Python object '{}' not found. Did you mean: {}?",
179                    reference.target,
180                    suggestions.join(", ")
181                )
182            };
183
184            ReferenceValidationResult {
185                reference: reference.clone(),
186                is_valid: false,
187                target_object: None,
188                error_message: Some(error_message),
189                suggestions,
190            }
191        }
192    }
193
194    fn get_all_objects(&self) -> Vec<&DomainObject> {
195        self.objects.values().collect()
196    }
197
198    fn clear_objects(&mut self) {
199        self.objects.clear();
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::domains::ReferenceLocation;
207
208    fn create_test_location() -> ReferenceLocation {
209        ReferenceLocation {
210            docname: "test.rst".to_string(),
211            lineno: Some(10),
212            column: Some(5),
213            source_path: Some("test.rst".to_string()),
214        }
215    }
216
217    #[test]
218    fn test_python_domain_creation() {
219        let domain = PythonDomain::new();
220        assert_eq!(domain.domain_name(), "python");
221        assert!(domain
222            .supported_reference_types()
223            .contains(&ReferenceType::Function));
224        assert!(domain
225            .supported_reference_types()
226            .contains(&ReferenceType::Class));
227    }
228
229    #[test]
230    fn test_register_function() {
231        let mut domain = PythonDomain::new();
232
233        let result = domain.register_function(
234            "test_func".to_string(),
235            "module.test_func".to_string(),
236            Some("test_func(x, y)".to_string()),
237            Some("Test function".to_string()),
238            create_test_location(),
239        );
240
241        assert!(result.is_ok());
242        assert_eq!(domain.objects.len(), 1);
243
244        let obj = domain.objects.get("module.test_func").unwrap();
245        assert_eq!(obj.name, "test_func");
246        assert_eq!(obj.object_type, "function");
247        assert_eq!(obj.domain, "python");
248    }
249
250    #[test]
251    fn test_reference_resolution() {
252        let mut domain = PythonDomain::new();
253
254        domain
255            .register_function(
256                "example".to_string(),
257                "mymodule.example".to_string(),
258                None,
259                None,
260                create_test_location(),
261            )
262            .unwrap();
263
264        let reference = CrossReference {
265            ref_type: ReferenceType::Function,
266            target: "mymodule.example".to_string(),
267            display_text: None,
268            source_location: create_test_location(),
269            is_external: false,
270        };
271
272        let resolved = domain.resolve_reference(&reference);
273        assert!(resolved.is_some());
274
275        let obj = resolved.unwrap();
276        assert_eq!(obj.qualified_name, "mymodule.example");
277        assert_eq!(obj.object_type, "function");
278    }
279
280    #[test]
281    fn test_reference_validation() {
282        let mut domain = PythonDomain::new();
283
284        domain
285            .register_class(
286                "TestClass".to_string(),
287                "module.TestClass".to_string(),
288                None,
289                create_test_location(),
290            )
291            .unwrap();
292
293        // Valid reference
294        let valid_ref = CrossReference {
295            ref_type: ReferenceType::Class,
296            target: "module.TestClass".to_string(),
297            display_text: None,
298            source_location: create_test_location(),
299            is_external: false,
300        };
301
302        let result = domain.validate_reference(&valid_ref);
303        assert!(result.is_valid);
304        assert!(result.target_object.is_some());
305        assert!(result.error_message.is_none());
306
307        // Invalid reference
308        let invalid_ref = CrossReference {
309            ref_type: ReferenceType::Class,
310            target: "nonexistent.Class".to_string(),
311            display_text: None,
312            source_location: create_test_location(),
313            is_external: false,
314        };
315
316        let result = domain.validate_reference(&invalid_ref);
317        assert!(!result.is_valid);
318        assert!(result.target_object.is_none());
319        assert!(result.error_message.is_some());
320    }
321
322    #[test]
323    fn test_suggestions() {
324        let mut domain = PythonDomain::new();
325
326        domain
327            .register_function(
328                "similar_function".to_string(),
329                "module.similar_function".to_string(),
330                None,
331                None,
332                create_test_location(),
333            )
334            .unwrap();
335
336        let reference = CrossReference {
337            ref_type: ReferenceType::Function,
338            target: "similar".to_string(),
339            display_text: None,
340            source_location: create_test_location(),
341            is_external: false,
342        };
343
344        let result = domain.validate_reference(&reference);
345        assert!(!result.is_valid);
346        assert!(!result.suggestions.is_empty());
347        assert!(result
348            .suggestions
349            .contains(&"module.similar_function".to_string()));
350    }
351}