1use anyhow::Result;
4use regex::Regex;
5use serde_json::Value;
6use std::collections::HashMap;
7
8#[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
20pub 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 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 let versioning_patterns = vec["']\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 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 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 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 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 };
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 fn extract_documentation(&self, content: &str, element: &str) -> Option<String> {
217 let lines: Vec<&str> = content.lines().collect();
219 for (i, line) in lines.iter().enumerate() {
220 if line.contains(element) {
221 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 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 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 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 pub fn is_public_api_element(&self, name: &str) -> bool {
280 !name.starts_with('_') && !name.starts_with("internal") && !name.starts_with("private")
282 }
283
284 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 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 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 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 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 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 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 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 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}