Skip to main content

tycode_core/analyzer/
rust_analyzer.rs

1use anyhow::{bail, Context, Result};
2use async_trait::async_trait;
3use cargo_metadata::MetadataCommand;
4use serde::Deserialize;
5use std::collections::HashSet;
6use std::path::{Path, PathBuf};
7use std::process::Stdio;
8use syn::{Item, ItemImpl, ItemTrait};
9use tokio::io::{AsyncBufReadExt, BufReader};
10use tokio::process::Command;
11
12use super::{BuildStatus, TypeAnalyzer};
13
14fn get_host_platform() -> Result<String> {
15    let output = std::process::Command::new("rustc")
16        .args(["-vV"])
17        .output()
18        .context("failed to run rustc -vV")?;
19
20    let stdout = String::from_utf8(output.stdout).context("rustc output is not valid UTF-8")?;
21
22    for line in stdout.lines() {
23        if let Some(host) = line.strip_prefix("host: ") {
24            return Ok(host.to_string());
25        }
26    }
27
28    bail!("could not determine host platform from rustc -vV output")
29}
30
31pub struct RustAnalyzer {
32    workspace_root: PathBuf,
33}
34
35struct ItemWithImpls {
36    item: Item,
37    impls: Vec<ItemImpl>,
38    traits: Vec<ItemTrait>,
39}
40
41impl RustAnalyzer {
42    pub fn new(workspace_root: PathBuf) -> Self {
43        RustAnalyzer { workspace_root }
44    }
45}
46
47#[async_trait]
48impl TypeAnalyzer for RustAnalyzer {
49    async fn search_types_by_name(&mut self, type_name: &str) -> Result<Vec<String>> {
50        let platform = get_host_platform()?;
51        let metadata = MetadataCommand::new()
52            .current_dir(&self.workspace_root)
53            .other_options(vec!["--filter-platform".to_string(), platform])
54            .exec()
55            .context("failed to run cargo metadata")?;
56
57        let mut results = Vec::new();
58        let limit = 20;
59
60        // Partition packages: workspace members first, then dependencies
61        let (workspace_packages, dependency_packages): (Vec<_>, Vec<_>) =
62            metadata.packages.iter().partition(|pkg| {
63                pkg.manifest_path
64                    .as_std_path()
65                    .starts_with(&self.workspace_root)
66            });
67
68        // Search workspace packages first
69        for package in &workspace_packages {
70            let crate_name = package.name.replace('-', "_");
71            let crate_root = package
72                .manifest_path
73                .parent()
74                .context("manifest has no parent directory")?;
75
76            let found = search_crate_for_type(&crate_root.into(), &crate_name, type_name, limit);
77            results.extend(found);
78
79            if results.len() >= limit {
80                results.truncate(limit);
81                break;
82            }
83        }
84
85        // Search dependencies if nothing found in workspace
86        if results.is_empty() {
87            for package in &dependency_packages {
88                let crate_name = package.name.replace('-', "_");
89                let crate_root = package
90                    .manifest_path
91                    .parent()
92                    .context("manifest has no parent directory")?;
93
94                let found =
95                    search_crate_for_type(&crate_root.into(), &crate_name, type_name, limit);
96                results.extend(found);
97
98                if results.len() >= limit {
99                    results.truncate(limit);
100                    break;
101                }
102            }
103        }
104
105        if results.is_empty() {
106            bail!("no types found matching '{}'", type_name);
107        }
108
109        Ok(results)
110    }
111
112    async fn get_type_docs(&mut self, type_path: &str) -> Result<String> {
113        let parts: Vec<&str> = type_path.split("::").collect();
114        if parts.is_empty() {
115            bail!("empty type path");
116        }
117
118        let crate_name = parts[0];
119        let item_path = &parts[1..];
120
121        let source_path = find_crate_source(crate_name, &self.workspace_root)?;
122        let item_with_impls = find_item_in_source(&source_path, item_path)?;
123        let code_outline = format_item_as_code(&item_with_impls);
124
125        Ok(code_outline)
126    }
127
128    async fn get_build_status(&mut self) -> Result<BuildStatus> {
129        let mut child = Command::new("cargo")
130            .args(["check", "--tests", "--message-format=json"])
131            .current_dir(&self.workspace_root)
132            .stdout(Stdio::piped())
133            .stderr(Stdio::null())
134            .spawn()
135            .context("failed to spawn cargo check")?;
136
137        let stdout = child.stdout.take().context("failed to capture stdout")?;
138        let reader = BufReader::new(stdout);
139        let mut lines = reader.lines();
140
141        let mut errors = Vec::new();
142        let mut warnings = Vec::new();
143
144        while let Some(line) = lines.next_line().await? {
145            let Ok(message) = serde_json::from_str::<CargoMessage>(&line) else {
146                continue;
147            };
148
149            let Some(compiler_message) = message.message else {
150                continue;
151            };
152
153            let formatted = format_compiler_message(&compiler_message);
154
155            match compiler_message.level.as_str() {
156                "error" => errors.push(formatted),
157                "warning" => warnings.push(formatted),
158                _ => {}
159            }
160        }
161
162        child.wait().await?;
163
164        Ok(BuildStatus { errors, warnings })
165    }
166}
167
168#[derive(Deserialize)]
169struct CargoMessage {
170    message: Option<CompilerMessage>,
171}
172
173#[derive(Deserialize)]
174struct CompilerMessage {
175    message: String,
176    level: String,
177    spans: Vec<CompilerSpan>,
178}
179
180#[derive(Deserialize)]
181struct CompilerSpan {
182    file_name: String,
183    line_start: u32,
184    column_start: u32,
185}
186
187fn format_compiler_message(msg: &CompilerMessage) -> String {
188    if msg.spans.is_empty() {
189        return msg.message.clone();
190    }
191
192    let span = &msg.spans[0];
193    format!(
194        "{}:{}:{}: {}",
195        span.file_name, span.line_start, span.column_start, msg.message
196    )
197}
198
199fn search_crate_for_type(
200    crate_root: &PathBuf,
201    crate_name: &str,
202    type_name: &str,
203    limit: usize,
204) -> Vec<String> {
205    // Run parsing in a thread with larger stack to handle complex syntax trees
206    let crate_root = crate_root.clone();
207    let crate_name = crate_name.to_string();
208    let type_name = type_name.to_string();
209
210    std::thread::Builder::new()
211        .stack_size(8 * 1024 * 1024) // 8MB stack
212        .spawn(move || search_crate_for_type_inner(&crate_root, &crate_name, &type_name, limit))
213        .ok()
214        .and_then(|handle| handle.join().ok())
215        .unwrap_or_default()
216}
217
218fn search_crate_for_type_inner(
219    crate_root: &PathBuf,
220    crate_name: &str,
221    type_name: &str,
222    limit: usize,
223) -> Vec<String> {
224    let lib_path = crate_root.join("src").join("lib.rs");
225    if !lib_path.exists() {
226        return Vec::new();
227    }
228
229    let Ok(content) = std::fs::read_to_string(&lib_path) else {
230        return Vec::new();
231    };
232
233    let file_size = content.len();
234
235    if file_size > 500_000 {
236        return Vec::new();
237    }
238
239    let Ok(file) = syn::parse_file(&content) else {
240        return Vec::new();
241    };
242
243    let mut results = Vec::new();
244    let src_dir = crate_root.join("src");
245    collect_matching_types_iterative(
246        &file.items,
247        type_name,
248        crate_name,
249        &src_dir,
250        &mut results,
251        limit,
252    );
253
254    results
255}
256
257/// Work item for iterative type search
258struct TypeSearchWork {
259    items: Vec<Item>,
260    path: String,
261    dir: PathBuf,
262}
263
264fn collect_matching_types_iterative(
265    initial_items: &[Item],
266    type_name: &str,
267    initial_path: &str,
268    initial_dir: &Path,
269    results: &mut Vec<String>,
270    limit: usize,
271) {
272    // Use explicit stack on heap instead of call stack
273    let mut work_stack: Vec<TypeSearchWork> = vec![TypeSearchWork {
274        items: initial_items.to_vec(),
275        path: initial_path.to_string(),
276        dir: initial_dir.to_path_buf(),
277    }];
278
279    while let Some(work) = work_stack.pop() {
280        if results.len() >= limit {
281            break;
282        }
283
284        for item in &work.items {
285            if results.len() >= limit {
286                break;
287            }
288
289            match item {
290                Item::Struct(s) if s.ident == type_name => {
291                    results.push(format!("{}::{}", work.path, s.ident));
292                }
293                Item::Enum(e) if e.ident == type_name => {
294                    results.push(format!("{}::{}", work.path, e.ident));
295                }
296                Item::Trait(t) if t.ident == type_name => {
297                    results.push(format!("{}::{}", work.path, t.ident));
298                }
299                Item::Type(t) if t.ident == type_name => {
300                    results.push(format!("{}::{}", work.path, t.ident));
301                }
302                Item::Union(u) if u.ident == type_name => {
303                    results.push(format!("{}::{}", work.path, u.ident));
304                }
305                Item::Fn(f) if f.sig.ident == type_name => {
306                    results.push(format!("{}::{}", work.path, f.sig.ident));
307                }
308                Item::Mod(m) => {
309                    let nested_path = format!("{}::{}", work.path, m.ident);
310
311                    if let Some((_, nested_items)) = &m.content {
312                        work_stack.push(TypeSearchWork {
313                            items: nested_items.clone(),
314                            path: nested_path,
315                            dir: work.dir.clone(),
316                        });
317                        continue;
318                    }
319
320                    let module_name = m.ident.to_string();
321                    let nested_items = match load_module_items(&module_name, &work.dir) {
322                        Ok(items) => items,
323                        Err(_e) => {
324                            // Silent failure for module loading - common for large/missing modules
325                            continue;
326                        }
327                    };
328                    let module_dir = resolve_module_dir(&module_name, &work.dir);
329                    work_stack.push(TypeSearchWork {
330                        items: nested_items,
331                        path: nested_path,
332                        dir: module_dir,
333                    });
334                }
335                _ => {}
336            }
337        }
338    }
339}
340
341fn find_crate_source(crate_name: &str, workspace_root: &PathBuf) -> Result<PathBuf> {
342    let platform = get_host_platform()?;
343    let metadata = MetadataCommand::new()
344        .current_dir(workspace_root)
345        .other_options(vec!["--filter-platform".to_string(), platform])
346        .exec()
347        .context("failed to run cargo metadata")?;
348
349    for package in &metadata.packages {
350        if crate_name == package.name.replace('-', "_") {
351            let parent = package
352                .manifest_path
353                .parent()
354                .context("manifest has no parent directory")?;
355            return Ok(parent.into());
356        }
357    }
358
359    bail!("crate '{}' not found in dependencies", crate_name)
360}
361
362fn find_item_in_source(crate_root: &PathBuf, item_path: &[&str]) -> Result<ItemWithImpls> {
363    let lib_path = crate_root.join("src").join("lib.rs");
364    if !lib_path.exists() {
365        bail!("lib.rs not found at {:?}", lib_path);
366    }
367
368    let content = std::fs::read_to_string(&lib_path).context("failed to read source file")?;
369
370    let file = syn::parse_file(&content).context("failed to parse source file")?;
371
372    if item_path.is_empty() {
373        bail!("no item specified");
374    }
375
376    let src_dir = crate_root.join("src");
377    let item = search_items_iterative(&file.items, item_path, &src_dir)?;
378    let item_name = extract_item_name(&item)?;
379    let impls = collect_impls_for_item_iterative(&file.items, &item_name, &src_dir);
380
381    let mut traits = Vec::new();
382    let mut seen_traits = HashSet::new();
383
384    for impl_item in &impls {
385        if let Some(trait_path) = extract_trait_path_from_impl(impl_item) {
386            if !is_std_trait(&trait_path) {
387                let trait_name = trait_path.split("::").last().unwrap_or(&trait_path);
388                if seen_traits.insert(trait_name.to_string()) {
389                    if let Some(trait_def) =
390                        find_trait_in_items_iterative(&file.items, trait_name, &src_dir)
391                    {
392                        traits.push(trait_def);
393                    }
394                }
395            }
396        }
397    }
398
399    Ok(ItemWithImpls {
400        item,
401        impls,
402        traits,
403    })
404}
405
406/// Work item for iterative item search
407struct ItemSearchWork {
408    items: Vec<Item>,
409    depth: usize,
410    dir: PathBuf,
411}
412
413fn search_items_iterative(
414    initial_items: &[Item],
415    path: &[&str],
416    initial_dir: &Path,
417) -> Result<Item> {
418    if path.is_empty() {
419        bail!("empty path");
420    }
421
422    let mut work_stack: Vec<ItemSearchWork> = vec![ItemSearchWork {
423        items: initial_items.to_vec(),
424        depth: 0,
425        dir: initial_dir.to_path_buf(),
426    }];
427
428    while let Some(work) = work_stack.pop() {
429        if work.depth >= path.len() {
430            continue;
431        }
432
433        let target_name = path[work.depth];
434        let is_final = work.depth == path.len() - 1;
435
436        for item in &work.items {
437            let matches = match item {
438                Item::Struct(s) => s.ident == target_name && is_final,
439                Item::Enum(e) => e.ident == target_name && is_final,
440                Item::Trait(t) => t.ident == target_name && is_final,
441                Item::Type(t) => t.ident == target_name && is_final,
442                Item::Union(u) => u.ident == target_name && is_final,
443                Item::Fn(f) => f.sig.ident == target_name && is_final,
444                Item::Mod(m) if m.ident == target_name && !is_final => {
445                    if let Some((_, nested_items)) = &m.content {
446                        work_stack.push(ItemSearchWork {
447                            items: nested_items.clone(),
448                            depth: work.depth + 1,
449                            dir: work.dir.clone(),
450                        });
451                        continue;
452                    }
453                    let Ok(nested_items) = load_module_items(&m.ident.to_string(), &work.dir)
454                    else {
455                        continue;
456                    };
457                    let module_dir = resolve_module_dir(&m.ident.to_string(), &work.dir);
458                    work_stack.push(ItemSearchWork {
459                        items: nested_items,
460                        depth: work.depth + 1,
461                        dir: module_dir,
462                    });
463                    continue;
464                }
465                _ => false,
466            };
467
468            if matches {
469                return Ok(item.clone());
470            }
471        }
472
473        // If we're at the final segment and didn't find it, search all modules
474        if is_final {
475            if let Some(found) = search_all_modules_iterative(&work.items, target_name, &work.dir) {
476                return Ok(found);
477            }
478        }
479    }
480
481    bail!("item '{}' not found", path.last().unwrap_or(&""))
482}
483
484fn extract_item_name(item: &Item) -> Result<String> {
485    let name = match item {
486        Item::Struct(s) => s.ident.to_string(),
487        Item::Enum(e) => e.ident.to_string(),
488        Item::Trait(t) => t.ident.to_string(),
489        Item::Type(t) => t.ident.to_string(),
490        Item::Union(u) => u.ident.to_string(),
491        Item::Fn(f) => f.sig.ident.to_string(),
492        _ => bail!("unsupported item type for name extraction"),
493    };
494    Ok(name)
495}
496
497/// Work item for iterative module search
498struct ModuleSearchWork {
499    items: Vec<Item>,
500    dir: PathBuf,
501}
502
503fn search_all_modules_iterative(
504    initial_items: &[Item],
505    target_name: &str,
506    initial_dir: &Path,
507) -> Option<Item> {
508    let mut work_stack: Vec<ModuleSearchWork> = vec![ModuleSearchWork {
509        items: initial_items.to_vec(),
510        dir: initial_dir.to_path_buf(),
511    }];
512
513    while let Some(work) = work_stack.pop() {
514        for item in &work.items {
515            match item {
516                Item::Struct(s) if s.ident == target_name => return Some(item.clone()),
517                Item::Enum(e) if e.ident == target_name => return Some(item.clone()),
518                Item::Trait(t) if t.ident == target_name => return Some(item.clone()),
519                Item::Type(t) if t.ident == target_name => return Some(item.clone()),
520                Item::Union(u) if u.ident == target_name => return Some(item.clone()),
521                Item::Mod(m) => {
522                    if let Some((_, nested_items)) = &m.content {
523                        work_stack.push(ModuleSearchWork {
524                            items: nested_items.clone(),
525                            dir: work.dir.clone(),
526                        });
527                        continue;
528                    }
529
530                    let Ok(nested_items) = load_module_items(&m.ident.to_string(), &work.dir)
531                    else {
532                        continue;
533                    };
534                    let module_dir = resolve_module_dir(&m.ident.to_string(), &work.dir);
535                    work_stack.push(ModuleSearchWork {
536                        items: nested_items,
537                        dir: module_dir,
538                    });
539                }
540                _ => {}
541            }
542        }
543    }
544    None
545}
546
547/// Work item for iterative impl collection
548struct ImplSearchWork {
549    items: Vec<Item>,
550    dir: PathBuf,
551}
552
553fn collect_impls_for_item_iterative(
554    initial_items: &[Item],
555    target_name: &str,
556    initial_dir: &Path,
557) -> Vec<ItemImpl> {
558    let mut impls = Vec::new();
559    let mut work_stack: Vec<ImplSearchWork> = vec![ImplSearchWork {
560        items: initial_items.to_vec(),
561        dir: initial_dir.to_path_buf(),
562    }];
563
564    while let Some(work) = work_stack.pop() {
565        for item in &work.items {
566            if let Item::Impl(impl_item) = item {
567                if impl_matches_type(impl_item, target_name) {
568                    impls.push(impl_item.clone());
569                }
570                continue;
571            }
572
573            let Item::Mod(m) = item else {
574                continue;
575            };
576
577            if let Some((_, nested_items)) = &m.content {
578                work_stack.push(ImplSearchWork {
579                    items: nested_items.clone(),
580                    dir: work.dir.clone(),
581                });
582                continue;
583            }
584
585            let Ok(nested_items) = load_module_items(&m.ident.to_string(), &work.dir) else {
586                continue;
587            };
588            let module_dir = resolve_module_dir(&m.ident.to_string(), &work.dir);
589            work_stack.push(ImplSearchWork {
590                items: nested_items,
591                dir: module_dir,
592            });
593        }
594    }
595
596    impls
597}
598
599fn impl_matches_type(impl_item: &ItemImpl, target_name: &str) -> bool {
600    if let syn::Type::Path(type_path) = &*impl_item.self_ty {
601        if let Some(segment) = type_path.path.segments.last() {
602            return segment.ident == target_name;
603        }
604    }
605    false
606}
607
608fn extract_trait_path_from_impl(impl_item: &ItemImpl) -> Option<String> {
609    impl_item.trait_.as_ref().map(|(_, path, _)| {
610        path.segments
611            .iter()
612            .map(|seg| seg.ident.to_string())
613            .collect::<Vec<_>>()
614            .join("::")
615    })
616}
617
618fn is_std_trait(trait_path: &str) -> bool {
619    trait_path.starts_with("std::")
620        || trait_path.starts_with("core::")
621        || trait_path.starts_with("alloc::")
622}
623
624fn load_module_items(module_name: &str, current_dir: &Path) -> Result<Vec<Item>> {
625    // Strip r# prefix for raw identifiers (e.g., r#trait -> trait)
626    let file_name = module_name.strip_prefix("r#").unwrap_or(module_name);
627
628    let mod_file = current_dir.join(format!("{}.rs", file_name));
629    let mod_dir_file = current_dir.join(file_name).join("mod.rs");
630
631    let path = if mod_file.exists() {
632        mod_file
633    } else if mod_dir_file.exists() {
634        mod_dir_file
635    } else {
636        bail!(
637            "module file not found for '{}' in {:?}",
638            module_name,
639            current_dir
640        );
641    };
642
643    let content =
644        std::fs::read_to_string(&path).context(format!("failed to read module file {:?}", path))?;
645
646    let file_size = content.len();
647
648    if file_size > 500_000 {
649        bail!("skipping large module (>500KB)");
650    }
651
652    let file =
653        syn::parse_file(&content).context(format!("failed to parse module file {:?}", path))?;
654
655    Ok(file.items)
656}
657
658fn resolve_module_dir(module_name: &str, current_dir: &Path) -> PathBuf {
659    // Strip r# prefix for raw identifiers
660    let file_name = module_name.strip_prefix("r#").unwrap_or(module_name);
661
662    let mod_file = current_dir.join(format!("{}.rs", file_name));
663    if mod_file.exists() {
664        return current_dir.join(file_name);
665    }
666    current_dir.join(file_name)
667}
668
669/// Work item for iterative trait search
670struct TraitSearchWork {
671    items: Vec<Item>,
672    dir: PathBuf,
673}
674
675fn find_trait_in_items_iterative(
676    initial_items: &[Item],
677    trait_name: &str,
678    initial_dir: &Path,
679) -> Option<ItemTrait> {
680    let mut work_stack: Vec<TraitSearchWork> = vec![TraitSearchWork {
681        items: initial_items.to_vec(),
682        dir: initial_dir.to_path_buf(),
683    }];
684
685    while let Some(work) = work_stack.pop() {
686        for item in &work.items {
687            match item {
688                Item::Trait(t) if t.ident == trait_name => {
689                    return Some(t.clone());
690                }
691                Item::Mod(m) => {
692                    if let Some((_, nested_items)) = &m.content {
693                        work_stack.push(TraitSearchWork {
694                            items: nested_items.clone(),
695                            dir: work.dir.clone(),
696                        });
697                        continue;
698                    }
699
700                    let Ok(nested_items) = load_module_items(&m.ident.to_string(), &work.dir)
701                    else {
702                        continue;
703                    };
704                    let module_dir = resolve_module_dir(&m.ident.to_string(), &work.dir);
705                    work_stack.push(TraitSearchWork {
706                        items: nested_items,
707                        dir: module_dir,
708                    });
709                }
710                _ => {}
711            }
712        }
713    }
714    None
715}
716
717fn format_item_as_code(item_with_impls: &ItemWithImpls) -> String {
718    let mut formatted_items = Vec::new();
719
720    for trait_item in &item_with_impls.traits {
721        let item = Item::Trait(strip_trait_bodies(trait_item));
722        let file = syn::File {
723            shebang: None,
724            attrs: vec![],
725            items: vec![item],
726        };
727        let formatted = prettyplease::unparse(&file);
728        let formatted = replace_empty_bodies_with_semicolons(&formatted);
729        formatted_items.push(add_blank_lines_between_items(&formatted));
730    }
731
732    let main_item = strip_implementations(&item_with_impls.item);
733    let file = syn::File {
734        shebang: None,
735        attrs: vec![],
736        items: vec![main_item],
737    };
738    formatted_items.push(prettyplease::unparse(&file));
739
740    for impl_item in &item_with_impls.impls {
741        let item = Item::Impl(strip_impl_bodies(impl_item));
742        let file = syn::File {
743            shebang: None,
744            attrs: vec![],
745            items: vec![item],
746        };
747        let formatted = prettyplease::unparse(&file);
748        let formatted = replace_empty_bodies_with_semicolons(&formatted);
749        formatted_items.push(add_blank_lines_between_items(&formatted));
750    }
751
752    let combined = formatted_items.join("\n\n");
753    format!("```rust\n{}```", combined)
754}
755
756fn strip_implementations(item: &Item) -> Item {
757    match item {
758        Item::Fn(f) => {
759            let mut func = f.clone();
760            func.block = Box::new(syn::Block {
761                brace_token: syn::token::Brace::default(),
762                stmts: vec![],
763            });
764            Item::Fn(func)
765        }
766        _ => item.clone(),
767    }
768}
769
770fn add_blank_lines_between_items(code: &str) -> String {
771    let lines: Vec<&str> = code.lines().collect();
772    let mut result = Vec::new();
773    let mut i = 0;
774
775    while i < lines.len() {
776        result.push(lines[i].to_string());
777
778        if i + 1 < lines.len() {
779            let current_line = lines[i].trim();
780            let next_line = lines[i + 1].trim();
781
782            let current_is_doc_comment = current_line.starts_with("///");
783            let next_is_doc_comment = next_line.starts_with("///");
784
785            if current_is_doc_comment && next_is_doc_comment {
786                i += 1;
787                continue;
788            }
789
790            let current_ends_item = current_line.ends_with('}') || current_line.ends_with(';');
791            let next_starts_new_item = next_line.starts_with("///")
792                || next_line.starts_with("#[")
793                || next_line.starts_with("pub fn")
794                || next_line.starts_with("fn")
795                || next_line.starts_with("async fn")
796                || next_line.starts_with("pub async fn")
797                || next_line.starts_with("pub(crate) fn")
798                || next_line.starts_with("pub(super) fn")
799                || next_line.starts_with("unsafe fn")
800                || next_line.starts_with("pub unsafe fn");
801
802            if current_ends_item && next_starts_new_item && !next_line.is_empty() {
803                result.push(String::new());
804            }
805        }
806
807        i += 1;
808    }
809
810    result.join("\n")
811}
812
813fn replace_empty_bodies_with_semicolons(code: &str) -> String {
814    let lines: Vec<&str> = code.lines().collect();
815    let mut result: Vec<String> = Vec::new();
816    let mut i = 0;
817
818    while i < lines.len() {
819        let line = lines[i];
820        let trimmed = line.trim();
821
822        if trimmed == "{}" {
823            if let Some(last_line) = result.last_mut() {
824                let last_trimmed = last_line.trim_end();
825                if last_trimmed.ends_with(',') {
826                    let without_comma = &last_trimmed[..last_trimmed.len() - 1];
827                    *last_line = format!(
828                        "{}{}",
829                        &last_line[..last_line.len() - last_trimmed.len()],
830                        without_comma.to_string() + ";"
831                    );
832                } else {
833                    *last_line = last_line.trim_end().to_string() + ";";
834                }
835            }
836            i += 1;
837            continue;
838        }
839
840        if trimmed.ends_with(" {}") {
841            let without_braces = &trimmed[..trimmed.len() - 3];
842            result.push(format!(
843                "{}{}",
844                &line[..line.len() - trimmed.len()],
845                without_braces.to_string() + ";"
846            ));
847        } else {
848            result.push(line.to_string());
849        }
850
851        i += 1;
852    }
853
854    result.join("\n")
855}
856
857fn strip_impl_bodies(impl_item: &ItemImpl) -> ItemImpl {
858    let mut impl_clone = impl_item.clone();
859
860    for item in &mut impl_clone.items {
861        if let syn::ImplItem::Fn(method) = item {
862            method.block = syn::Block {
863                brace_token: syn::token::Brace::default(),
864                stmts: vec![],
865            };
866        }
867    }
868
869    impl_clone
870}
871
872fn strip_trait_bodies(trait_item: &ItemTrait) -> ItemTrait {
873    let mut trait_clone = trait_item.clone();
874
875    for item in &mut trait_clone.items {
876        if let syn::TraitItem::Fn(method) = item {
877            method.default = None;
878        }
879    }
880
881    trait_clone
882}