codeprism_analysis/
api_surface.rs

1//! API surface analysis module
2
3use anyhow::Result;
4use regex::Regex;
5use serde_json::Value;
6use std::collections::HashMap;
7
8/// API element information
9#[derive(Debug, Clone)]
10pub struct ApiElement {
11    pub element_type: String,
12    pub name: String,
13    pub visibility: String,
14    pub signature: Option<String>,
15    pub documentation: Option<String>,
16    pub deprecated: bool,
17    pub breaking_change_risk: String,
18}
19
20/// API surface analyzer
21pub struct ApiSurfaceAnalyzer {
22    patterns: HashMap<String, Vec<ApiPattern>>,
23}
24
25#[derive(Debug, Clone)]
26struct ApiPattern {
27    _name: String,
28    pattern: Regex,
29    element_type: String,
30    visibility_pattern: Option<Regex>,
31}
32
33impl ApiSurfaceAnalyzer {
34    pub fn new() -> Self {
35        let mut analyzer = Self {
36            patterns: HashMap::new(),
37        };
38        analyzer.initialize_patterns();
39        analyzer
40    }
41
42    fn initialize_patterns(&mut self) {
43        // Public API patterns
44        let public_api_patterns = vec![
45            ApiPattern {
46                _name: "Public Function".to_string(),
47                pattern: Regex::new(r"(?m)^(pub\s+)?(?:async\s+)?fn\s+(\w+)\s*\([^)]*\)").unwrap(),
48                element_type: "function".to_string(),
49                visibility_pattern: Some(Regex::new(r"^pub\s+").unwrap()),
50            },
51            ApiPattern {
52                _name: "Public Class".to_string(),
53                pattern: Regex::new(r"(?m)^(pub\s+)?(?:class|struct)\s+(\w+)").unwrap(),
54                element_type: "class".to_string(),
55                visibility_pattern: Some(Regex::new(r"^pub\s+").unwrap()),
56            },
57            ApiPattern {
58                _name: "Public Method".to_string(),
59                pattern: Regex::new(
60                    r"(?m)^\s*(pub\s+)?(?:async\s+)?(?:def|fn)\s+(\w+)\s*\([^)]*\)",
61                )
62                .unwrap(),
63                element_type: "method".to_string(),
64                visibility_pattern: Some(Regex::new(r"^\s*pub\s+").unwrap()),
65            },
66            ApiPattern {
67                _name: "Public Constant".to_string(),
68                pattern: Regex::new(r"(?m)^(pub\s+)?const\s+(\w+)").unwrap(),
69                element_type: "constant".to_string(),
70                visibility_pattern: Some(Regex::new(r"^pub\s+").unwrap()),
71            },
72        ];
73        self.patterns
74            .insert("public_api".to_string(), public_api_patterns);
75
76        // Versioning patterns
77        let versioning_patterns = vec![
78            ApiPattern {
79                _name: "Version Annotation".to_string(),
80                pattern: Regex::new(r#"@version\s*\(\s*["']([\d.]+)["']\s*\)"#).unwrap(),
81                element_type: "version".to_string(),
82                visibility_pattern: None,
83            },
84            ApiPattern {
85                _name: "Since Annotation".to_string(),
86                pattern: Regex::new(r#"@since\s*\(\s*["']([\d.]+)["']\s*\)"#).unwrap(),
87                element_type: "version".to_string(),
88                visibility_pattern: None,
89            },
90            ApiPattern {
91                _name: "Deprecated Annotation".to_string(),
92                pattern: Regex::new(r"@deprecated|#\[deprecated\]|@Deprecated").unwrap(),
93                element_type: "deprecated".to_string(),
94                visibility_pattern: None,
95            },
96        ];
97        self.patterns
98            .insert("versioning".to_string(), versioning_patterns);
99
100        // Breaking change patterns
101        let breaking_change_patterns = vec![
102            ApiPattern {
103                _name: "Parameter Change".to_string(),
104                pattern: Regex::new(r"(?m)fn\s+\w+\s*\([^)]*\w+\s*:\s*\w+[^)]*\)").unwrap(),
105                element_type: "breaking_change".to_string(),
106                visibility_pattern: None,
107            },
108            ApiPattern {
109                _name: "Return Type Change".to_string(),
110                pattern: Regex::new(r"(?m)fn\s+\w+\s*\([^)]*\)\s*->\s*\w+").unwrap(),
111                element_type: "breaking_change".to_string(),
112                visibility_pattern: None,
113            },
114        ];
115        self.patterns
116            .insert("breaking_changes".to_string(), breaking_change_patterns);
117
118        // Documentation patterns
119        let documentation_patterns = vec![
120            ApiPattern {
121                _name: "Doc Comment".to_string(),
122                pattern: Regex::new(r#"(?m)^\s*///.*$|^\s*#.*$|^\s*""".*?""""#).unwrap(),
123                element_type: "documentation".to_string(),
124                visibility_pattern: None,
125            },
126            ApiPattern {
127                _name: "Missing Documentation".to_string(),
128                pattern: Regex::new(r#"(?m)^(pub\s+)?(?:fn|class|struct)\s+\w+"#).unwrap(),
129                element_type: "missing_docs".to_string(),
130                visibility_pattern: Some(Regex::new(r"^pub\s+").unwrap()),
131            },
132        ];
133        self.patterns
134            .insert("documentation".to_string(), documentation_patterns);
135
136        // Compatibility patterns
137        let compatibility_patterns = vec![
138            ApiPattern {
139                _name: "Generic Type".to_string(),
140                pattern: Regex::new(
141                    r"<[A-Z]\w*(?:\s*:\s*\w+)?(?:\s*,\s*[A-Z]\w*(?:\s*:\s*\w+)?)*>",
142                )
143                .unwrap(),
144                element_type: "generic".to_string(),
145                visibility_pattern: None,
146            },
147            ApiPattern {
148                _name: "Optional Parameter".to_string(),
149                pattern: Regex::new(r"\w+\s*:\s*Option<\w+>|\w+\s*=\s*\w+").unwrap(),
150                element_type: "optional".to_string(),
151                visibility_pattern: None,
152            },
153        ];
154        self.patterns
155            .insert("compatibility".to_string(), compatibility_patterns);
156    }
157
158    /// Analyze API surface
159    pub fn analyze_api_surface(
160        &self,
161        content: &str,
162        analysis_types: &[String],
163        include_private_apis: bool,
164    ) -> Result<Vec<ApiElement>> {
165        let mut elements = Vec::new();
166
167        let target_types = if analysis_types.contains(&"all".to_string()) {
168            self.patterns.keys().cloned().collect::<Vec<_>>()
169        } else {
170            analysis_types.to_vec()
171        };
172
173        for analysis_type in target_types {
174            if let Some(patterns) = self.patterns.get(&analysis_type) {
175                for pattern in patterns {
176                    for captures in pattern.pattern.captures_iter(content) {
177                        let full_match = captures.get(0).unwrap().as_str();
178                        let name = captures
179                            .get(2)
180                            .or_else(|| captures.get(1))
181                            .map(|m| m.as_str())
182                            .unwrap_or("unknown");
183
184                        let is_public =
185                            if let Some(visibility_pattern) = &pattern.visibility_pattern {
186                                visibility_pattern.is_match(full_match)
187                            } else {
188                                true // Assume public if no visibility pattern
189                            };
190
191                        if is_public || include_private_apis {
192                            elements.push(ApiElement {
193                                element_type: pattern.element_type.clone(),
194                                name: name.to_string(),
195                                visibility: if is_public {
196                                    "public".to_string()
197                                } else {
198                                    "private".to_string()
199                                },
200                                signature: Some(full_match.to_string()),
201                                documentation: self.extract_documentation(content, full_match),
202                                deprecated: self.is_deprecated(content, full_match),
203                                breaking_change_risk: self
204                                    .assess_breaking_change_risk(&pattern.element_type),
205                            });
206                        }
207                    }
208                }
209            }
210        }
211
212        Ok(elements)
213    }
214
215    /// Extract documentation for an API element
216    fn extract_documentation(&self, content: &str, element: &str) -> Option<String> {
217        // Look for documentation comments above the element
218        let lines: Vec<&str> = content.lines().collect();
219        for (i, line) in lines.iter().enumerate() {
220            if line.contains(element) {
221                // Look backwards for documentation
222                let mut docs = Vec::new();
223                for j in (0..i).rev() {
224                    let prev_line = lines[j].trim();
225                    if prev_line.starts_with("///")
226                        || prev_line.starts_with("#")
227                        || prev_line.starts_with("\"\"\"")
228                    {
229                        docs.insert(0, prev_line);
230                    } else if !prev_line.is_empty() {
231                        break;
232                    }
233                }
234                if !docs.is_empty() {
235                    return Some(docs.join("\n"));
236                }
237            }
238        }
239        None
240    }
241
242    /// Check if an element is deprecated
243    fn is_deprecated(&self, content: &str, element: &str) -> bool {
244        let lines: Vec<&str> = content.lines().collect();
245        for (i, line) in lines.iter().enumerate() {
246            if line.contains(element) {
247                // Look backwards for deprecation annotations
248                for j in (0..i).rev() {
249                    let prev_line = lines[j].trim();
250                    if prev_line.contains("@deprecated")
251                        || prev_line.contains("#[deprecated]")
252                        || prev_line.contains("@Deprecated")
253                    {
254                        return true;
255                    } else if !prev_line.is_empty()
256                        && !prev_line.starts_with("///")
257                        && !prev_line.starts_with("#")
258                    {
259                        break;
260                    }
261                }
262            }
263        }
264        false
265    }
266
267    /// Assess breaking change risk
268    fn assess_breaking_change_risk(&self, element_type: &str) -> String {
269        match element_type {
270            "function" | "method" => "medium".to_string(),
271            "class" | "struct" => "high".to_string(),
272            "constant" => "low".to_string(),
273            "generic" => "high".to_string(),
274            _ => "low".to_string(),
275        }
276    }
277
278    /// Check if element is considered public API
279    pub fn is_public_api_element(&self, name: &str) -> bool {
280        // Simple heuristic - in practice, this would be more sophisticated
281        !name.starts_with('_') && !name.starts_with("internal") && !name.starts_with("private")
282    }
283
284    /// Get API recommendations
285    pub fn get_api_recommendations(&self, elements: &[ApiElement]) -> Vec<String> {
286        let mut recommendations = Vec::new();
287
288        if elements.is_empty() {
289            recommendations.push(
290                "No API elements detected. Consider defining clear public interfaces.".to_string(),
291            );
292            return recommendations;
293        }
294
295        // Count different types of issues
296        let public_elements = elements.iter().filter(|e| e.visibility == "public").count();
297        let documented_elements = elements
298            .iter()
299            .filter(|e| e.documentation.is_some())
300            .count();
301        let deprecated_elements = elements.iter().filter(|e| e.deprecated).count();
302
303        if public_elements > 0 {
304            let documentation_coverage =
305                (documented_elements as f64 / public_elements as f64) * 100.0;
306
307            if documentation_coverage < 80.0 {
308                recommendations.push(format!(
309                    "API documentation coverage is {documentation_coverage:.1}%. Consider documenting more public APIs."
310                ));
311            }
312        }
313
314        if deprecated_elements > 0 {
315            recommendations.push(format!(
316                "{deprecated_elements} deprecated API elements found. Plan migration strategy for users."
317            ));
318        }
319
320        // Check for high-risk breaking changes
321        let high_risk_elements = elements
322            .iter()
323            .filter(|e| e.breaking_change_risk == "high")
324            .count();
325        if high_risk_elements > 0 {
326            recommendations.push(format!(
327                "{high_risk_elements} high-risk API elements detected. Changes to these may break compatibility."
328            ));
329        }
330
331        recommendations.push("Use semantic versioning for API changes.".to_string());
332        recommendations.push("Consider API versioning strategy for major changes.".to_string());
333        recommendations.push("Implement API compatibility testing.".to_string());
334        recommendations.push("Document API lifecycle and deprecation policies.".to_string());
335
336        recommendations
337    }
338
339    /// Analyze public API elements
340    pub fn analyze_public_api(&self, content: &str) -> Result<Vec<Value>> {
341        let elements = self.analyze_api_surface(content, &["public_api".to_string()], false)?;
342
343        Ok(elements
344            .into_iter()
345            .map(|e| {
346                serde_json::json!({
347                    "type": e.element_type,
348                    "name": e.name,
349                    "visibility": e.visibility,
350                    "signature": e.signature,
351                    "documented": e.documentation.is_some(),
352                    "deprecated": e.deprecated,
353                    "breaking_change_risk": e.breaking_change_risk
354                })
355            })
356            .collect())
357    }
358
359    /// Analyze API versioning
360    pub fn analyze_api_versioning(&self, content: &str) -> Result<Vec<Value>> {
361        let elements = self.analyze_api_surface(content, &["versioning".to_string()], true)?;
362
363        Ok(elements
364            .into_iter()
365            .map(|e| {
366                serde_json::json!({
367                    "type": e.element_type,
368                    "name": e.name,
369                    "signature": e.signature,
370                    "deprecated": e.deprecated
371                })
372            })
373            .collect())
374    }
375
376    /// Detect potential breaking changes
377    pub fn detect_api_breaking_changes(&self, content: &str) -> Result<Vec<Value>> {
378        let elements =
379            self.analyze_api_surface(content, &["breaking_changes".to_string()], false)?;
380
381        Ok(elements
382            .into_iter()
383            .map(|e| {
384                serde_json::json!({
385                    "type": e.element_type,
386                    "name": e.name,
387                    "signature": e.signature,
388                    "risk_level": e.breaking_change_risk,
389                    "recommendation": match e.breaking_change_risk.as_str() {
390                        "high" => "Major version bump recommended",
391                        "medium" => "Minor version bump may be needed",
392                        _ => "Patch version acceptable"
393                    }
394                })
395            })
396            .collect())
397    }
398
399    /// Analyze documentation coverage
400    pub fn analyze_api_documentation_coverage(&self, content: &str) -> Result<Vec<Value>> {
401        let elements = self.analyze_api_surface(content, &["public_api".to_string()], false)?;
402
403        let total_public = elements.len();
404        let documented = elements
405            .iter()
406            .filter(|e| e.documentation.is_some())
407            .count();
408        let coverage = if total_public > 0 {
409            (documented as f64 / total_public as f64) * 100.0
410        } else {
411            100.0
412        };
413
414        Ok(vec![serde_json::json!({
415            "total_public_apis": total_public,
416            "documented_apis": documented,
417            "coverage_percentage": coverage,
418            "undocumented_apis": elements.into_iter()
419                .filter(|e| e.documentation.is_none())
420                .map(|e| serde_json::json!({
421                    "name": e.name,
422                    "type": e.element_type,
423                    "signature": e.signature
424                }))
425                .collect::<Vec<_>>()
426        })])
427    }
428}
429
430impl Default for ApiSurfaceAnalyzer {
431    fn default() -> Self {
432        Self::new()
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_public_function_detection() {
442        let analyzer = ApiSurfaceAnalyzer::new();
443
444        let code = "pub fn test_function(x: i32) -> i32 { x + 1 }";
445        let elements = analyzer
446            .analyze_api_surface(code, &["public_api".to_string()], false)
447            .unwrap();
448
449        assert!(!elements.is_empty(), "Should find API surface elements");
450
451        // Verify elements contain the expected API surface
452        assert!(!elements.is_empty(), "Should have at least one API element");
453        assert!(
454            elements
455                .iter()
456                .any(|e| e.name == "test_function" && e.visibility == "public"),
457            "Should find public test_function"
458        );
459
460        // Verify element structure is meaningful
461        let test_func = elements
462            .iter()
463            .find(|e| e.name == "test_function")
464            .expect("Should have test_function");
465        assert!(
466            test_func.signature.is_some() && !test_func.signature.as_ref().unwrap().is_empty(),
467            "Function should have signature"
468        );
469        assert!(
470            !test_func.element_type.is_empty(),
471            "Function should have element type"
472        );
473    }
474
475    #[test]
476    fn test_deprecated_detection() {
477        let analyzer = ApiSurfaceAnalyzer::new();
478
479        let code = "#[deprecated]\npub fn old_function() {}";
480        let elements = analyzer
481            .analyze_api_surface(code, &["public_api".to_string()], false)
482            .unwrap();
483
484        assert!(!elements.is_empty(), "Should not be empty");
485        assert!(elements.iter().any(|e| e.deprecated));
486    }
487
488    #[test]
489    fn test_documentation_extraction() {
490        let analyzer = ApiSurfaceAnalyzer::new();
491
492        let code = "/// This is a test function\npub fn documented_function() {}";
493        let elements = analyzer
494            .analyze_api_surface(code, &["public_api".to_string()], false)
495            .unwrap();
496
497        assert!(!elements.is_empty(), "Should not be empty");
498        assert!(elements.iter().any(|e| e.documentation.is_some()));
499    }
500
501    #[test]
502    fn test_breaking_change_risk_assessment() {
503        let analyzer = ApiSurfaceAnalyzer::new();
504
505        assert_eq!(analyzer.assess_breaking_change_risk("class"), "high");
506        assert_eq!(analyzer.assess_breaking_change_risk("function"), "medium");
507        assert_eq!(analyzer.assess_breaking_change_risk("constant"), "low");
508    }
509
510    #[test]
511    fn test_public_api_element_check() {
512        let analyzer = ApiSurfaceAnalyzer::new();
513
514        assert!(analyzer.is_public_api_element("public_function"));
515        assert!(!analyzer.is_public_api_element("_private_function"));
516        assert!(!analyzer.is_public_api_element("internal_function"));
517    }
518
519    #[test]
520    fn test_api_recommendations() {
521        let analyzer = ApiSurfaceAnalyzer::new();
522
523        let elements = vec![ApiElement {
524            element_type: "function".to_string(),
525            name: "test".to_string(),
526            visibility: "public".to_string(),
527            signature: None,
528            documentation: None,
529            deprecated: false,
530            breaking_change_risk: "medium".to_string(),
531        }];
532
533        let recommendations = analyzer.get_api_recommendations(&elements);
534        assert!(!recommendations.is_empty(), "Should not be empty");
535    }
536}