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 {:.1}%. Consider documenting more public APIs.",
310                    documentation_coverage
311                ));
312            }
313        }
314
315        if deprecated_elements > 0 {
316            recommendations.push(format!(
317                "{} deprecated API elements found. Plan migration strategy for users.",
318                deprecated_elements
319            ));
320        }
321
322        // Check for high-risk breaking changes
323        let high_risk_elements = elements
324            .iter()
325            .filter(|e| e.breaking_change_risk == "high")
326            .count();
327        if high_risk_elements > 0 {
328            recommendations.push(format!(
329                "{} high-risk API elements detected. Changes to these may break compatibility.",
330                high_risk_elements
331            ));
332        }
333
334        recommendations.push("Use semantic versioning for API changes.".to_string());
335        recommendations.push("Consider API versioning strategy for major changes.".to_string());
336        recommendations.push("Implement API compatibility testing.".to_string());
337        recommendations.push("Document API lifecycle and deprecation policies.".to_string());
338
339        recommendations
340    }
341
342    /// Analyze public API elements
343    pub fn analyze_public_api(&self, content: &str) -> Result<Vec<Value>> {
344        let elements = self.analyze_api_surface(content, &["public_api".to_string()], false)?;
345
346        Ok(elements
347            .into_iter()
348            .map(|e| {
349                serde_json::json!({
350                    "type": e.element_type,
351                    "name": e.name,
352                    "visibility": e.visibility,
353                    "signature": e.signature,
354                    "documented": e.documentation.is_some(),
355                    "deprecated": e.deprecated,
356                    "breaking_change_risk": e.breaking_change_risk
357                })
358            })
359            .collect())
360    }
361
362    /// Analyze API versioning
363    pub fn analyze_api_versioning(&self, content: &str) -> Result<Vec<Value>> {
364        let elements = self.analyze_api_surface(content, &["versioning".to_string()], true)?;
365
366        Ok(elements
367            .into_iter()
368            .map(|e| {
369                serde_json::json!({
370                    "type": e.element_type,
371                    "name": e.name,
372                    "signature": e.signature,
373                    "deprecated": e.deprecated
374                })
375            })
376            .collect())
377    }
378
379    /// Detect potential breaking changes
380    pub fn detect_api_breaking_changes(&self, content: &str) -> Result<Vec<Value>> {
381        let elements =
382            self.analyze_api_surface(content, &["breaking_changes".to_string()], false)?;
383
384        Ok(elements
385            .into_iter()
386            .map(|e| {
387                serde_json::json!({
388                    "type": e.element_type,
389                    "name": e.name,
390                    "signature": e.signature,
391                    "risk_level": e.breaking_change_risk,
392                    "recommendation": match e.breaking_change_risk.as_str() {
393                        "high" => "Major version bump recommended",
394                        "medium" => "Minor version bump may be needed",
395                        _ => "Patch version acceptable"
396                    }
397                })
398            })
399            .collect())
400    }
401
402    /// Analyze documentation coverage
403    pub fn analyze_api_documentation_coverage(&self, content: &str) -> Result<Vec<Value>> {
404        let elements = self.analyze_api_surface(content, &["public_api".to_string()], false)?;
405
406        let total_public = elements.len();
407        let documented = elements
408            .iter()
409            .filter(|e| e.documentation.is_some())
410            .count();
411        let coverage = if total_public > 0 {
412            (documented as f64 / total_public as f64) * 100.0
413        } else {
414            100.0
415        };
416
417        Ok(vec![serde_json::json!({
418            "total_public_apis": total_public,
419            "documented_apis": documented,
420            "coverage_percentage": coverage,
421            "undocumented_apis": elements.into_iter()
422                .filter(|e| e.documentation.is_none())
423                .map(|e| serde_json::json!({
424                    "name": e.name,
425                    "type": e.element_type,
426                    "signature": e.signature
427                }))
428                .collect::<Vec<_>>()
429        })])
430    }
431}
432
433impl Default for ApiSurfaceAnalyzer {
434    fn default() -> Self {
435        Self::new()
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_public_function_detection() {
445        let analyzer = ApiSurfaceAnalyzer::new();
446
447        let code = "pub fn test_function(x: i32) -> i32 { x + 1 }";
448        let elements = analyzer
449            .analyze_api_surface(code, &["public_api".to_string()], false)
450            .unwrap();
451
452        assert!(!elements.is_empty());
453        assert!(elements
454            .iter()
455            .any(|e| e.name == "test_function" && e.visibility == "public"));
456    }
457
458    #[test]
459    fn test_deprecated_detection() {
460        let analyzer = ApiSurfaceAnalyzer::new();
461
462        let code = "#[deprecated]\npub fn old_function() {}";
463        let elements = analyzer
464            .analyze_api_surface(code, &["public_api".to_string()], false)
465            .unwrap();
466
467        assert!(!elements.is_empty());
468        assert!(elements.iter().any(|e| e.deprecated));
469    }
470
471    #[test]
472    fn test_documentation_extraction() {
473        let analyzer = ApiSurfaceAnalyzer::new();
474
475        let code = "/// This is a test function\npub fn documented_function() {}";
476        let elements = analyzer
477            .analyze_api_surface(code, &["public_api".to_string()], false)
478            .unwrap();
479
480        assert!(!elements.is_empty());
481        assert!(elements.iter().any(|e| e.documentation.is_some()));
482    }
483
484    #[test]
485    fn test_breaking_change_risk_assessment() {
486        let analyzer = ApiSurfaceAnalyzer::new();
487
488        assert_eq!(analyzer.assess_breaking_change_risk("class"), "high");
489        assert_eq!(analyzer.assess_breaking_change_risk("function"), "medium");
490        assert_eq!(analyzer.assess_breaking_change_risk("constant"), "low");
491    }
492
493    #[test]
494    fn test_public_api_element_check() {
495        let analyzer = ApiSurfaceAnalyzer::new();
496
497        assert!(analyzer.is_public_api_element("public_function"));
498        assert!(!analyzer.is_public_api_element("_private_function"));
499        assert!(!analyzer.is_public_api_element("internal_function"));
500    }
501
502    #[test]
503    fn test_api_recommendations() {
504        let analyzer = ApiSurfaceAnalyzer::new();
505
506        let elements = vec![ApiElement {
507            element_type: "function".to_string(),
508            name: "test".to_string(),
509            visibility: "public".to_string(),
510            signature: None,
511            documentation: None,
512            deprecated: false,
513            breaking_change_risk: "medium".to_string(),
514        }];
515
516        let recommendations = analyzer.get_api_recommendations(&elements);
517        assert!(!recommendations.is_empty());
518    }
519}