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 {:.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 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 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 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 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 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}