1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use anyhow::Result;
5use tokmd_analysis_types::{ApiExportItem, ApiSurfaceReport, LangApiSurface, ModuleApiRow};
6use tokmd_types::{ExportData, FileKind, FileRow};
7
8use tokmd_analysis_util::{AnalysisLimits, normalize_path};
9
10const DEFAULT_MAX_FILE_BYTES: u64 = 128 * 1024;
11const MAX_TOP_EXPORTERS: usize = 20;
12const MAX_BY_MODULE: usize = 50;
13
14fn is_api_surface_lang(lang: &str) -> bool {
16 matches!(
17 lang.to_lowercase().as_str(),
18 "rust" | "javascript" | "typescript" | "python" | "go" | "java"
19 )
20}
21
22#[derive(Debug)]
24struct Symbol {
25 is_public: bool,
26 is_documented: bool,
27}
28
29fn extract_symbols(lang: &str, text: &str) -> Vec<Symbol> {
31 let lines: Vec<&str> = text.lines().collect();
32 match lang.to_lowercase().as_str() {
33 "rust" => extract_rust_symbols(&lines),
34 "javascript" | "typescript" => extract_js_ts_symbols(&lines),
35 "python" => extract_python_symbols(&lines),
36 "go" => extract_go_symbols(&lines),
37 "java" => extract_java_symbols(&lines),
38 _ => Vec::new(),
39 }
40}
41
42fn has_doc_comment(lines: &[&str], idx: usize) -> bool {
44 if idx == 0 {
45 return false;
46 }
47 let prev = lines[idx - 1].trim();
48 prev.starts_with("///")
53 || prev.starts_with("//!")
54 || prev.starts_with("/**")
55 || prev.starts_with("#[doc")
56 || prev.starts_with("/// ")
57 || prev.starts_with("// ")
58 || prev.starts_with("\"\"\"")
59 || prev.starts_with("'''")
60}
61
62fn extract_rust_symbols(lines: &[&str]) -> Vec<Symbol> {
67 let mut symbols = Vec::new();
68
69 for (i, line) in lines.iter().enumerate() {
70 let trimmed = line.trim();
71 if trimmed.starts_with("//") || trimmed.starts_with('*') || trimmed.starts_with("/*") {
73 continue;
74 }
75
76 let is_public = is_rust_pub_item(trimmed);
77 let is_internal = is_rust_internal_item(trimmed);
78
79 if is_public || is_internal {
80 symbols.push(Symbol {
81 is_public,
82 is_documented: has_doc_comment(lines, i),
83 });
84 }
85 }
86
87 symbols
88}
89
90fn is_rust_pub_item(trimmed: &str) -> bool {
91 if !trimmed.starts_with("pub ") && !trimmed.starts_with("pub(") {
93 return false;
94 }
95
96 let after_pub = if trimmed.starts_with("pub(") {
98 if let Some(close) = trimmed.find(')') {
100 trimmed[close + 1..].trim_start()
101 } else {
102 return false;
103 }
104 } else {
105 &trimmed[4..]
107 };
108
109 after_pub.starts_with("fn ")
111 || after_pub.starts_with("struct ")
112 || after_pub.starts_with("enum ")
113 || after_pub.starts_with("trait ")
114 || after_pub.starts_with("type ")
115 || after_pub.starts_with("const ")
116 || after_pub.starts_with("static ")
117 || after_pub.starts_with("mod ")
118 || after_pub.starts_with("async fn ")
119 || after_pub.starts_with("unsafe fn ")
120 || after_pub.starts_with("unsafe trait ")
121}
122
123fn is_rust_internal_item(trimmed: &str) -> bool {
124 if trimmed.starts_with("pub ") || trimmed.starts_with("pub(") {
127 return false;
128 }
129
130 trimmed.starts_with("fn ")
131 || trimmed.starts_with("struct ")
132 || trimmed.starts_with("enum ")
133 || trimmed.starts_with("trait ")
134 || trimmed.starts_with("type ")
135 || trimmed.starts_with("const ")
136 || trimmed.starts_with("static ")
137 || trimmed.starts_with("mod ")
138 || trimmed.starts_with("async fn ")
139 || trimmed.starts_with("unsafe fn ")
140 || trimmed.starts_with("unsafe trait ")
141}
142
143fn extract_js_ts_symbols(lines: &[&str]) -> Vec<Symbol> {
148 let mut symbols = Vec::new();
149
150 for (i, line) in lines.iter().enumerate() {
151 let trimmed = line.trim();
152
153 if trimmed.starts_with("//") || trimmed.starts_with('*') || trimmed.starts_with("/*") {
154 continue;
155 }
156
157 let is_public = is_js_export(trimmed);
158 let is_internal = !is_public && is_js_internal(trimmed);
159
160 if is_public || is_internal {
161 symbols.push(Symbol {
162 is_public,
163 is_documented: has_doc_comment(lines, i),
164 });
165 }
166 }
167
168 symbols
169}
170
171fn is_js_export(trimmed: &str) -> bool {
172 trimmed.starts_with("export function ")
173 || trimmed.starts_with("export async function ")
174 || trimmed.starts_with("export class ")
175 || trimmed.starts_with("export const ")
176 || trimmed.starts_with("export let ")
177 || trimmed.starts_with("export default ")
178 || trimmed.starts_with("export interface ")
179 || trimmed.starts_with("export type ")
180 || trimmed.starts_with("export enum ")
181 || trimmed.starts_with("export abstract class ")
182}
183
184fn is_js_internal(trimmed: &str) -> bool {
185 trimmed.starts_with("function ")
186 || trimmed.starts_with("async function ")
187 || trimmed.starts_with("class ")
188 || trimmed.starts_with("const ")
189 || trimmed.starts_with("let ")
190 || trimmed.starts_with("interface ")
191 || trimmed.starts_with("type ")
192 || trimmed.starts_with("enum ")
193}
194
195fn extract_python_symbols(lines: &[&str]) -> Vec<Symbol> {
200 let mut symbols = Vec::new();
201
202 for (i, line) in lines.iter().enumerate() {
203 let trimmed = line.trim();
204
205 if line.starts_with(' ') || line.starts_with('\t') {
207 continue;
208 }
209 if trimmed.starts_with('#') || trimmed.is_empty() {
210 continue;
211 }
212
213 let is_symbol = trimmed.starts_with("def ")
214 || trimmed.starts_with("async def ")
215 || trimmed.starts_with("class ");
216
217 if is_symbol {
218 let name = extract_python_name(trimmed);
219 let is_public = !name.starts_with('_');
220 let documented = has_python_docstring(lines, i);
221 symbols.push(Symbol {
222 is_public,
223 is_documented: documented || has_doc_comment(lines, i),
224 });
225 }
226 }
227
228 symbols
229}
230
231fn extract_python_name(trimmed: &str) -> String {
232 let rest = if let Some(r) = trimmed.strip_prefix("async def ") {
233 r
234 } else if let Some(r) = trimmed.strip_prefix("def ") {
235 r
236 } else if let Some(r) = trimmed.strip_prefix("class ") {
237 r
238 } else {
239 return String::new();
240 };
241
242 rest.chars()
243 .take_while(|c| c.is_alphanumeric() || *c == '_')
244 .collect()
245}
246
247fn has_python_docstring(lines: &[&str], idx: usize) -> bool {
249 for line in lines.iter().take((idx + 3).min(lines.len())).skip(idx + 1) {
251 let t = line.trim();
252 if t.is_empty() {
253 continue;
254 }
255 return t.starts_with("\"\"\"") || t.starts_with("'''") || t.starts_with("r\"\"\"");
256 }
257 false
258}
259
260fn extract_go_symbols(lines: &[&str]) -> Vec<Symbol> {
265 let mut symbols = Vec::new();
266
267 for (i, line) in lines.iter().enumerate() {
268 let trimmed = line.trim();
269
270 if trimmed.starts_with("//") || trimmed.starts_with("/*") {
271 continue;
272 }
273
274 if let Some(name) = extract_go_item_name(trimmed) {
275 let first_char = name.chars().next().unwrap_or('_');
277 let is_public = first_char.is_uppercase();
278 symbols.push(Symbol {
279 is_public,
280 is_documented: has_doc_comment(lines, i),
281 });
282 }
283 }
284
285 symbols
286}
287
288fn extract_go_item_name(trimmed: &str) -> Option<String> {
289 if let Some(rest) = trimmed.strip_prefix("func ") {
291 let rest = if rest.starts_with('(') {
292 if let Some(close) = rest.find(')') {
294 rest[close + 1..].trim_start()
295 } else {
296 return None;
297 }
298 } else {
299 rest
300 };
301 let name: String = rest
302 .chars()
303 .take_while(|c| c.is_alphanumeric() || *c == '_')
304 .collect();
305 if !name.is_empty() {
306 return Some(name);
307 }
308 }
309
310 if let Some(rest) = trimmed.strip_prefix("type ") {
312 let name: String = rest
313 .chars()
314 .take_while(|c| c.is_alphanumeric() || *c == '_')
315 .collect();
316 if !name.is_empty() {
317 return Some(name);
318 }
319 }
320
321 for prefix in &["var ", "const "] {
323 if let Some(rest) = trimmed.strip_prefix(prefix) {
324 let name: String = rest
325 .chars()
326 .take_while(|c| c.is_alphanumeric() || *c == '_')
327 .collect();
328 if !name.is_empty() {
329 return Some(name);
330 }
331 }
332 }
333
334 None
335}
336
337fn extract_java_symbols(lines: &[&str]) -> Vec<Symbol> {
342 let mut symbols = Vec::new();
343
344 for (i, line) in lines.iter().enumerate() {
345 let trimmed = line.trim();
346
347 if trimmed.starts_with("//") || trimmed.starts_with('*') || trimmed.starts_with("/*") {
348 continue;
349 }
350
351 let is_public = is_java_public(trimmed);
352 let is_internal = !is_public && is_java_internal(trimmed);
353
354 if is_public || is_internal {
355 symbols.push(Symbol {
356 is_public,
357 is_documented: has_doc_comment(lines, i),
358 });
359 }
360 }
361
362 symbols
363}
364
365fn is_java_public(trimmed: &str) -> bool {
366 trimmed.starts_with("public class ")
367 || trimmed.starts_with("public interface ")
368 || trimmed.starts_with("public enum ")
369 || trimmed.starts_with("public static ")
370 || trimmed.starts_with("public abstract class ")
371 || trimmed.starts_with("public final class ")
372 || trimmed.starts_with("public record ")
373 || trimmed.starts_with("public sealed ")
374 || (trimmed.starts_with("public ")
376 && (trimmed.contains('(') || trimmed.contains(" class ") || trimmed.contains(" interface ")))
377}
378
379fn is_java_internal(trimmed: &str) -> bool {
380 trimmed.starts_with("private ")
382 || trimmed.starts_with("protected ")
383 || trimmed.starts_with("class ")
384 || trimmed.starts_with("interface ")
385 || trimmed.starts_with("enum ")
386 || trimmed.starts_with("abstract class ")
387 || trimmed.starts_with("final class ")
388 || trimmed.starts_with("static ")
389 || trimmed.starts_with("record ")
390}
391
392pub fn build_api_surface_report(
398 root: &Path,
399 files: &[PathBuf],
400 export: &ExportData,
401 limits: &AnalysisLimits,
402) -> Result<ApiSurfaceReport> {
403 let mut row_map: BTreeMap<String, &FileRow> = BTreeMap::new();
405 for row in export.rows.iter().filter(|r| r.kind == FileKind::Parent) {
406 row_map.insert(normalize_path(&row.path, root), row);
407 }
408
409 let per_file_limit = limits.max_file_bytes.unwrap_or(DEFAULT_MAX_FILE_BYTES) as usize;
410 let mut total_bytes = 0u64;
411
412 let mut total_items = 0usize;
414 let mut public_items = 0usize;
415 let mut internal_items = 0usize;
416 let mut documented_public = 0usize;
417
418 let mut lang_totals: BTreeMap<String, (usize, usize, usize)> = BTreeMap::new(); let mut module_totals: BTreeMap<String, (usize, usize)> = BTreeMap::new(); let mut exporters: Vec<ApiExportItem> = Vec::new();
426
427 for rel in files {
428 if limits.max_bytes.is_some_and(|limit| total_bytes >= limit) {
429 break;
430 }
431
432 let rel_str = normalize_path(&rel.to_string_lossy(), root);
433 let row = match row_map.get(&rel_str) {
434 Some(r) => *r,
435 None => continue,
436 };
437
438 if !is_api_surface_lang(&row.lang) {
439 continue;
440 }
441
442 let path = root.join(rel);
443 let bytes = match tokmd_content::read_head(&path, per_file_limit) {
444 Ok(b) => b,
445 Err(_) => continue,
446 };
447 total_bytes += bytes.len() as u64;
448
449 if !tokmd_content::is_text_like(&bytes) {
450 continue;
451 }
452
453 let text = String::from_utf8_lossy(&bytes);
454 let symbols = extract_symbols(&row.lang, &text);
455
456 if symbols.is_empty() {
457 continue;
458 }
459
460 let file_public: usize = symbols.iter().filter(|s| s.is_public).count();
461 let file_internal: usize = symbols.iter().filter(|s| !s.is_public).count();
462 let file_documented: usize = symbols
463 .iter()
464 .filter(|s| s.is_public && s.is_documented)
465 .count();
466 let file_total = symbols.len();
467
468 total_items += file_total;
469 public_items += file_public;
470 internal_items += file_internal;
471 documented_public += file_documented;
472
473 let lang_key = row.lang.clone();
475 let entry = lang_totals.entry(lang_key).or_insert((0, 0, 0));
476 entry.0 += file_total;
477 entry.1 += file_public;
478 entry.2 += file_internal;
479
480 let mod_entry = module_totals.entry(row.module.clone()).or_insert((0, 0));
482 mod_entry.0 += file_total;
483 mod_entry.1 += file_public;
484
485 if file_public > 0 {
487 exporters.push(ApiExportItem {
488 path: rel_str,
489 lang: row.lang.clone(),
490 public_items: file_public,
491 total_items: file_total,
492 });
493 }
494 }
495
496 let by_language: BTreeMap<String, LangApiSurface> = lang_totals
498 .into_iter()
499 .map(|(lang, (total, public, internal))| {
500 let public_ratio = if total == 0 {
501 0.0
502 } else {
503 round_f64(public as f64 / total as f64, 4)
504 };
505 (
506 lang,
507 LangApiSurface {
508 total_items: total,
509 public_items: public,
510 internal_items: internal,
511 public_ratio,
512 },
513 )
514 })
515 .collect();
516
517 let mut by_module: Vec<ModuleApiRow> = module_totals
519 .into_iter()
520 .map(|(module, (total, public))| {
521 let public_ratio = if total == 0 {
522 0.0
523 } else {
524 round_f64(public as f64 / total as f64, 4)
525 };
526 ModuleApiRow {
527 module,
528 total_items: total,
529 public_items: public,
530 public_ratio,
531 }
532 })
533 .collect();
534 by_module.sort_by(|a, b| {
535 b.total_items
536 .cmp(&a.total_items)
537 .then_with(|| a.module.cmp(&b.module))
538 });
539 by_module.truncate(MAX_BY_MODULE);
540
541 exporters.sort_by(|a, b| {
543 b.public_items
544 .cmp(&a.public_items)
545 .then_with(|| a.path.cmp(&b.path))
546 });
547 exporters.truncate(MAX_TOP_EXPORTERS);
548
549 let public_ratio = if total_items == 0 {
550 0.0
551 } else {
552 round_f64(public_items as f64 / total_items as f64, 4)
553 };
554
555 let documented_ratio = if public_items == 0 {
556 0.0
557 } else {
558 round_f64(documented_public as f64 / public_items as f64, 4)
559 };
560
561 Ok(ApiSurfaceReport {
562 total_items,
563 public_items,
564 internal_items,
565 public_ratio,
566 documented_ratio,
567 by_language,
568 by_module,
569 top_exporters: exporters,
570 })
571}
572
573fn round_f64(val: f64, decimals: u32) -> f64 {
574 let factor = 10f64.powi(decimals as i32);
575 (val * factor).round() / factor
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
587 fn rust_pub_fn() {
588 let code = "pub fn foo() {\n}\n";
589 let syms = extract_symbols("rust", code);
590 assert_eq!(syms.len(), 1);
591 assert!(syms[0].is_public);
592 }
593
594 #[test]
595 fn rust_private_fn() {
596 let code = "fn bar() {\n}\n";
597 let syms = extract_symbols("rust", code);
598 assert_eq!(syms.len(), 1);
599 assert!(!syms[0].is_public);
600 }
601
602 #[test]
603 fn rust_pub_struct_enum_trait() {
604 let code = "pub struct Foo;\npub enum Bar {}\npub trait Baz {}\n";
605 let syms = extract_symbols("rust", code);
606 assert_eq!(syms.len(), 3);
607 assert!(syms.iter().all(|s| s.is_public));
608 }
609
610 #[test]
611 fn rust_pub_crate() {
612 let code = "pub(crate) fn internal_fn() {\n}\n";
613 let syms = extract_symbols("rust", code);
614 assert_eq!(syms.len(), 1);
615 assert!(syms[0].is_public);
617 }
618
619 #[test]
620 fn rust_internal_items() {
621 let code = "struct Private;\nenum InternalEnum {}\ntrait InternalTrait {}\n";
622 let syms = extract_symbols("rust", code);
623 assert_eq!(syms.len(), 3);
624 assert!(syms.iter().all(|s| !s.is_public));
625 }
626
627 #[test]
628 fn rust_documented_item() {
629 let code = "/// Documentation\npub fn documented() {\n}\n";
630 let syms = extract_symbols("rust", code);
631 assert_eq!(syms.len(), 1);
632 assert!(syms[0].is_public);
633 assert!(syms[0].is_documented);
634 }
635
636 #[test]
637 fn rust_undocumented_item() {
638 let code = "pub fn undocumented() {\n}\n";
639 let syms = extract_symbols("rust", code);
640 assert_eq!(syms.len(), 1);
641 assert!(syms[0].is_public);
642 assert!(!syms[0].is_documented);
643 }
644
645 #[test]
646 fn rust_pub_mod_const_static() {
647 let code = "pub mod mymod;\npub const X: u32 = 1;\npub static Y: &str = \"hi\";\n";
648 let syms = extract_symbols("rust", code);
649 assert_eq!(syms.len(), 3);
650 assert!(syms.iter().all(|s| s.is_public));
651 }
652
653 #[test]
654 fn rust_pub_type_alias() {
655 let code = "pub type MyResult = Result<(), Error>;\n";
656 let syms = extract_symbols("rust", code);
657 assert_eq!(syms.len(), 1);
658 assert!(syms[0].is_public);
659 }
660
661 #[test]
662 fn rust_async_unsafe() {
663 let code = "pub async fn async_pub() {}\npub unsafe fn unsafe_pub() {}\n";
664 let syms = extract_symbols("rust", code);
665 assert_eq!(syms.len(), 2);
666 assert!(syms.iter().all(|s| s.is_public));
667 }
668
669 #[test]
674 fn js_export_function() {
675 let code = "export function foo() {\n}\n";
676 let syms = extract_symbols("javascript", code);
677 assert_eq!(syms.len(), 1);
678 assert!(syms[0].is_public);
679 }
680
681 #[test]
682 fn js_export_class() {
683 let code = "export class MyClass {\n}\n";
684 let syms = extract_symbols("typescript", code);
685 assert_eq!(syms.len(), 1);
686 assert!(syms[0].is_public);
687 }
688
689 #[test]
690 fn js_export_const_default() {
691 let code = "export const X = 1;\nexport default function main() {}\n";
692 let syms = extract_symbols("javascript", code);
693 assert_eq!(syms.len(), 2);
694 assert!(syms.iter().all(|s| s.is_public));
695 }
696
697 #[test]
698 fn ts_export_interface_type_enum() {
699 let code =
700 "export interface IFoo {}\nexport type Bar = string;\nexport enum Baz { A, B }\n";
701 let syms = extract_symbols("typescript", code);
702 assert_eq!(syms.len(), 3);
703 assert!(syms.iter().all(|s| s.is_public));
704 }
705
706 #[test]
707 fn js_internal_function() {
708 let code = "function internal() {\n}\n";
709 let syms = extract_symbols("javascript", code);
710 assert_eq!(syms.len(), 1);
711 assert!(!syms[0].is_public);
712 }
713
714 #[test]
719 fn python_public_def() {
720 let code = "def public_func():\n pass\n";
721 let syms = extract_symbols("python", code);
722 assert_eq!(syms.len(), 1);
723 assert!(syms[0].is_public);
724 }
725
726 #[test]
727 fn python_private_def() {
728 let code = "def _private_func():\n pass\n";
729 let syms = extract_symbols("python", code);
730 assert_eq!(syms.len(), 1);
731 assert!(!syms[0].is_public);
732 }
733
734 #[test]
735 fn python_class() {
736 let code = "class MyClass:\n pass\n";
737 let syms = extract_symbols("python", code);
738 assert_eq!(syms.len(), 1);
739 assert!(syms[0].is_public);
740 }
741
742 #[test]
743 fn python_private_class() {
744 let code = "class _InternalClass:\n pass\n";
745 let syms = extract_symbols("python", code);
746 assert_eq!(syms.len(), 1);
747 assert!(!syms[0].is_public);
748 }
749
750 #[test]
751 fn python_indented_def_ignored() {
752 let code = "class Foo:\n def method(self):\n pass\n";
753 let syms = extract_symbols("python", code);
754 assert_eq!(syms.len(), 1);
756 assert!(syms[0].is_public);
757 }
758
759 #[test]
760 fn python_docstring_detected() {
761 let code = "def documented():\n \"\"\"This is documented.\"\"\"\n pass\n";
762 let syms = extract_symbols("python", code);
763 assert_eq!(syms.len(), 1);
764 assert!(syms[0].is_documented);
765 }
766
767 #[test]
772 fn go_public_func() {
773 let code = "func PublicFunc() {\n}\n";
774 let syms = extract_symbols("go", code);
775 assert_eq!(syms.len(), 1);
776 assert!(syms[0].is_public);
777 }
778
779 #[test]
780 fn go_private_func() {
781 let code = "func privateFunc() {\n}\n";
782 let syms = extract_symbols("go", code);
783 assert_eq!(syms.len(), 1);
784 assert!(!syms[0].is_public);
785 }
786
787 #[test]
788 fn go_public_type() {
789 let code = "type MyStruct struct {\n}\n";
790 let syms = extract_symbols("go", code);
791 assert_eq!(syms.len(), 1);
792 assert!(syms[0].is_public);
793 }
794
795 #[test]
796 fn go_method_receiver() {
797 let code = "func (s *Server) Handle() {\n}\n";
798 let syms = extract_symbols("go", code);
799 assert_eq!(syms.len(), 1);
800 assert!(syms[0].is_public);
801 }
802
803 #[test]
804 fn go_private_method() {
805 let code = "func (s *Server) handle() {\n}\n";
806 let syms = extract_symbols("go", code);
807 assert_eq!(syms.len(), 1);
808 assert!(!syms[0].is_public);
809 }
810
811 #[test]
816 fn java_public_class() {
817 let code = "public class MyClass {\n}\n";
818 let syms = extract_symbols("java", code);
819 assert_eq!(syms.len(), 1);
820 assert!(syms[0].is_public);
821 }
822
823 #[test]
824 fn java_public_interface() {
825 let code = "public interface MyInterface {\n}\n";
826 let syms = extract_symbols("java", code);
827 assert_eq!(syms.len(), 1);
828 assert!(syms[0].is_public);
829 }
830
831 #[test]
832 fn java_public_enum() {
833 let code = "public enum Color {\n RED, GREEN, BLUE\n}\n";
834 let syms = extract_symbols("java", code);
835 assert_eq!(syms.len(), 1);
836 assert!(syms[0].is_public);
837 }
838
839 #[test]
840 fn java_public_static_method() {
841 let code = "public static void main(String[] args) {\n}\n";
842 let syms = extract_symbols("java", code);
843 assert_eq!(syms.len(), 1);
844 assert!(syms[0].is_public);
845 }
846
847 #[test]
848 fn java_package_private_class() {
849 let code = "class InternalClass {\n}\n";
850 let syms = extract_symbols("java", code);
851 assert_eq!(syms.len(), 1);
852 assert!(!syms[0].is_public);
853 }
854
855 #[test]
856 fn java_private_member() {
857 let code = "private void helper() {\n}\n";
858 let syms = extract_symbols("java", code);
859 assert_eq!(syms.len(), 1);
860 assert!(!syms[0].is_public);
861 }
862
863 #[test]
864 fn java_documented() {
865 let code = "/** Javadoc */\npublic class Documented {\n}\n";
866 let syms = extract_symbols("java", code);
867 assert_eq!(syms.len(), 1);
868 assert!(syms[0].is_documented);
869 }
870
871 #[test]
876 fn unsupported_lang_returns_empty() {
877 let code = "some code here\n";
878 let syms = extract_symbols("markdown", code);
879 assert!(syms.is_empty());
880 }
881
882 #[test]
883 fn empty_input_returns_empty() {
884 for lang in &["rust", "javascript", "typescript", "python", "go", "java"] {
885 let syms = extract_symbols(lang, "");
886 assert!(
887 syms.is_empty(),
888 "empty input for {lang} should yield no symbols"
889 );
890 }
891 }
892
893 #[test]
898 fn supported_langs() {
899 assert!(is_api_surface_lang("Rust"));
900 assert!(is_api_surface_lang("JavaScript"));
901 assert!(is_api_surface_lang("TypeScript"));
902 assert!(is_api_surface_lang("Python"));
903 assert!(is_api_surface_lang("Go"));
904 assert!(is_api_surface_lang("Java"));
905 }
906
907 #[test]
908 fn supported_langs_case_insensitive() {
909 assert!(is_api_surface_lang("RUST"));
910 assert!(is_api_surface_lang("javascript"));
911 assert!(is_api_surface_lang("gO"));
912 }
913
914 #[test]
915 fn unsupported_langs() {
916 assert!(!is_api_surface_lang("Markdown"));
917 assert!(!is_api_surface_lang("JSON"));
918 assert!(!is_api_surface_lang("CSS"));
919 }
920
921 #[test]
926 fn has_doc_comment_at_index_zero_is_false() {
927 let lines = vec!["pub fn foo() {}"];
928 assert!(!has_doc_comment(&lines, 0));
929 }
930
931 #[test]
932 fn has_doc_comment_with_doc_attribute() {
933 let lines = vec!["#[doc = \"documented\"]", "pub fn foo() {}"];
934 assert!(has_doc_comment(&lines, 1));
935 }
936
937 #[test]
942 fn go_var_public() {
943 let code = "var PublicVar int = 42\n";
944 let syms = extract_symbols("go", code);
945 assert_eq!(syms.len(), 1);
946 assert!(syms[0].is_public);
947 }
948
949 #[test]
950 fn go_const_private() {
951 let code = "const maxBuffer = 1024\n";
952 let syms = extract_symbols("go", code);
953 assert_eq!(syms.len(), 1);
954 assert!(!syms[0].is_public);
955 }
956
957 #[test]
962 fn python_async_def() {
963 let code = "async def fetch():\n pass\n";
964 let syms = extract_symbols("python", code);
965 assert_eq!(syms.len(), 1);
966 assert!(syms[0].is_public);
967 }
968
969 #[test]
970 fn python_async_def_private() {
971 let code = "async def _fetch():\n pass\n";
972 let syms = extract_symbols("python", code);
973 assert_eq!(syms.len(), 1);
974 assert!(!syms[0].is_public);
975 }
976
977 #[test]
982 fn java_public_record() {
983 let code = "public record Point(int x, int y) {}\n";
984 let syms = extract_symbols("java", code);
985 assert_eq!(syms.len(), 1);
986 assert!(syms[0].is_public);
987 }
988
989 #[test]
990 fn java_protected_member() {
991 let code = "protected void helper() {}\n";
992 let syms = extract_symbols("java", code);
993 assert_eq!(syms.len(), 1);
994 assert!(!syms[0].is_public);
995 }
996
997 #[test]
1002 fn ts_export_enum() {
1003 let code = "export enum Direction { Up, Down }\n";
1004 let syms = extract_symbols("typescript", code);
1005 assert_eq!(syms.len(), 1);
1006 assert!(syms[0].is_public);
1007 }
1008
1009 #[test]
1010 fn js_async_function_internal() {
1011 let code = "async function doWork() {}\n";
1012 let syms = extract_symbols("javascript", code);
1013 assert_eq!(syms.len(), 1);
1014 assert!(!syms[0].is_public);
1015 }
1016
1017 #[test]
1022 fn rust_pub_unmatched_paren_no_panic() {
1023 let code = "pub(broken fn foo() {}\n";
1024 let syms = extract_symbols("rust", code);
1025 assert!(syms.is_empty() || !syms[0].is_public);
1027 }
1028
1029 #[test]
1034 fn test_round() {
1035 assert_eq!(round_f64(0.12345, 4), 0.1235);
1036 assert_eq!(round_f64(0.5, 0), 1.0);
1037 assert_eq!(round_f64(1.0, 4), 1.0);
1038 }
1039
1040 #[test]
1041 fn test_round_zero() {
1042 assert_eq!(round_f64(0.0, 4), 0.0);
1043 }
1044
1045 #[test]
1046 fn test_round_small_fraction() {
1047 assert_eq!(round_f64(0.3333, 2), 0.33);
1048 assert_eq!(round_f64(0.6667, 2), 0.67);
1049 }
1050}