1use crate::stdlib_mapper::{StdlibMapper, WasmCompatibility};
18use crate::external_packages::ExternalPackageRegistry;
19use rustpython_parser::{ast, Parse};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
25pub struct PythonImport {
26 pub module: String,
28
29 pub items: Vec<ImportedSymbol>,
32
33 pub import_type: ImportType,
35
36 pub alias: Option<String>,
38
39 pub level: usize,
41
42 pub line: usize,
44
45 pub col_offset: usize,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
51pub struct ImportedSymbol {
52 pub name: String,
54
55 pub alias: Option<String>,
57}
58
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
61pub enum ImportType {
62 Module,
64
65 FromImport,
67
68 StarImport,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct RustDependency {
75 pub crate_name: String,
77
78 pub version: String,
80
81 pub features: Vec<String>,
83
84 pub wasm_compat: WasmCompatibility,
86
87 pub target: Option<String>,
89
90 pub notes: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ImportAnalysis {
97 pub python_imports: Vec<PythonImport>,
99
100 pub rust_dependencies: Vec<RustDependency>,
102
103 pub rust_use_statements: Vec<String>,
105
106 pub wasm_compatibility: WasmCompatibilitySummary,
108
109 pub unmapped_modules: Vec<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct WasmCompatibilitySummary {
116 pub fully_compatible: bool,
118
119 pub needs_wasi: bool,
121
122 pub needs_js_interop: bool,
124
125 pub has_incompatible: bool,
127
128 pub modules_by_compat: HashMap<String, WasmCompatibility>,
130}
131
132pub struct ImportAnalyzer {
145 stdlib_mapper: StdlibMapper,
146 external_registry: ExternalPackageRegistry,
147 current_module_path: Option<String>,
149}
150
151impl ImportAnalyzer {
152 pub fn new() -> Self {
154 Self {
155 stdlib_mapper: StdlibMapper::new(),
156 external_registry: ExternalPackageRegistry::new(),
157 current_module_path: None,
158 }
159 }
160
161 pub fn with_module_path(module_path: String) -> Self {
163 Self {
164 stdlib_mapper: StdlibMapper::new(),
165 external_registry: ExternalPackageRegistry::new(),
166 current_module_path: Some(module_path),
167 }
168 }
169
170 pub fn set_module_path(&mut self, module_path: String) {
172 self.current_module_path = Some(module_path);
173 }
174
175 pub fn analyze(&self, python_code: &str) -> ImportAnalysis {
177 let python_imports = self.extract_imports(python_code);
178 let mut rust_dependencies = Vec::new();
179 let mut rust_use_statements = Vec::new();
180 let mut unmapped_modules = Vec::new();
181 let mut modules_by_compat = HashMap::new();
182
183 for import in &python_imports {
184 if let Some(module_mapping) = self.stdlib_mapper.get_module(&import.module) {
186 let rust_dep = RustDependency {
188 crate_name: module_mapping.rust_crate.clone()
189 .unwrap_or_else(|| "std".to_string()),
190 version: module_mapping.version.clone(),
191 features: vec![],
192 wasm_compat: module_mapping.wasm_compatible.clone(),
193 target: None,
194 notes: module_mapping.notes.clone(),
195 };
196
197 modules_by_compat.insert(
199 import.module.clone(),
200 module_mapping.wasm_compatible.clone()
201 );
202
203 if !module_mapping.rust_use.is_empty() {
205 let use_stmt = if import.items.is_empty() {
206 format!("use {};", module_mapping.rust_use)
207 } else {
208 let items_str = import.items
210 .iter()
211 .map(|sym| {
212 if let Some(ref alias) = sym.alias {
213 format!("{} as {}", sym.name, alias)
214 } else {
215 sym.name.clone()
216 }
217 })
218 .collect::<Vec<_>>()
219 .join(", ");
220 format!("use {}::{{{}}};", module_mapping.rust_use, items_str)
221 };
222 rust_use_statements.push(use_stmt);
223 }
224
225 if module_mapping.rust_crate.is_some() {
227 rust_dependencies.push(rust_dep);
228 }
229 }
230 else if let Some(pkg_mapping) = self.external_registry.get_package(&import.module) {
232 let rust_dep = RustDependency {
233 crate_name: pkg_mapping.rust_crate.clone(),
234 version: pkg_mapping.version.clone(),
235 features: pkg_mapping.features.clone(),
236 wasm_compat: pkg_mapping.wasm_compatible.clone(),
237 target: None,
238 notes: pkg_mapping.notes.clone(),
239 };
240
241 modules_by_compat.insert(
242 import.module.clone(),
243 pkg_mapping.wasm_compatible.clone()
244 );
245
246 rust_use_statements.push(format!("use {};", pkg_mapping.rust_crate));
248
249 rust_dependencies.push(rust_dep);
250 } else {
251 unmapped_modules.push(import.module.clone());
253 }
254 }
255
256 rust_dependencies.sort_by(|a, b| a.crate_name.cmp(&b.crate_name));
258 rust_dependencies.dedup_by(|a, b| a.crate_name == b.crate_name);
259
260 rust_use_statements.sort();
261 rust_use_statements.dedup();
262
263 unmapped_modules.sort();
264 unmapped_modules.dedup();
265
266 let wasm_compatibility = self.build_wasm_summary(&modules_by_compat);
268
269 ImportAnalysis {
270 python_imports,
271 rust_dependencies,
272 rust_use_statements,
273 wasm_compatibility,
274 unmapped_modules,
275 }
276 }
277
278 fn extract_imports(&self, python_code: &str) -> Vec<PythonImport> {
283 let mut imports = Vec::new();
284
285 let parsed = match ast::Suite::parse(python_code, "<input>") {
287 Ok(suite) => suite,
288 Err(e) => {
289 eprintln!("Failed to parse Python code: {}", e);
291 return imports;
292 }
293 };
294
295 for stmt in parsed.iter() {
297 match stmt {
298 ast::Stmt::Import(import_stmt) => {
300 self.extract_import_stmt(import_stmt, &mut imports);
301 }
302 ast::Stmt::ImportFrom(import_from_stmt) => {
304 self.extract_import_from_stmt(import_from_stmt, &mut imports);
305 }
306 _ => {}
307 }
308 }
309
310 imports
311 }
312
313 fn extract_import_stmt(&self, import_stmt: &ast::StmtImport, imports: &mut Vec<PythonImport>) {
320 for alias in &import_stmt.names {
321 let module = alias.name.to_string();
322 let alias_name = alias.asname.as_ref().map(|a| a.to_string());
323
324 imports.push(PythonImport {
325 module,
326 items: vec![],
327 import_type: ImportType::Module,
328 alias: alias_name,
329 level: 0, line: 0, col_offset: 0,
332 });
333 }
334 }
335
336 fn extract_import_from_stmt(&self, import_from: &ast::StmtImportFrom, imports: &mut Vec<PythonImport>) {
346 let module = if let Some(module_identifier) = &import_from.module {
348 module_identifier.to_string()
349 } else {
350 String::new()
352 };
353
354 let level: usize = if let Some(level_int) = &import_from.level {
358 let level_str = format!("{:?}", level_int);
361 if let Some(start) = level_str.find('(') {
363 if let Some(end) = level_str.find(')') {
364 let number_str = &level_str[start + 1..end];
365 number_str.parse().unwrap_or(0)
366 } else {
367 0
368 }
369 } else {
370 level_str.parse().unwrap_or(0)
372 }
373 } else {
374 0
375 };
376
377 let resolved_module = self.resolve_relative_import(&module, level);
379
380 let is_star_import = import_from.names.iter().any(|alias| alias.name.as_str() == "*");
382
383 if is_star_import {
384 imports.push(PythonImport {
386 module: resolved_module,
387 items: vec![],
388 import_type: ImportType::StarImport,
389 alias: None,
390 level,
391 line: 0, col_offset: 0,
393 });
394 } else {
395 let items: Vec<ImportedSymbol> = import_from
397 .names
398 .iter()
399 .map(|alias| ImportedSymbol {
400 name: alias.name.to_string(),
401 alias: alias.asname.as_ref().map(|a| a.to_string()),
402 })
403 .collect();
404
405 imports.push(PythonImport {
406 module: resolved_module,
407 items,
408 import_type: ImportType::FromImport,
409 alias: None,
410 level,
411 line: 0, col_offset: 0,
413 });
414 }
415 }
416
417 fn resolve_relative_import(&self, module: &str, level: usize) -> String {
427 if level == 0 {
428 return module.to_string();
430 }
431
432 if let Some(ref current_path) = self.current_module_path {
434 let path_parts: Vec<&str> = current_path.split('.').collect();
435
436 let parent_depth = path_parts.len().saturating_sub(level);
438 let parent_path = path_parts[..parent_depth].join(".");
439
440 if module.is_empty() {
441 parent_path
443 } else {
444 if parent_path.is_empty() {
446 module.to_string()
447 } else {
448 format!("{}.{}", parent_path, module)
449 }
450 }
451 } else {
452 if level == 1 {
454 format!(".{}", module)
455 } else {
456 format!("{}{}", ".".repeat(level), module)
457 }
458 }
459 }
460
461
462 fn build_wasm_summary(
464 &self,
465 modules_by_compat: &HashMap<String, WasmCompatibility>
466 ) -> WasmCompatibilitySummary {
467 let mut needs_wasi = false;
468 let mut needs_js_interop = false;
469 let mut has_incompatible = false;
470
471 for compat in modules_by_compat.values() {
472 match compat {
473 WasmCompatibility::RequiresWasi => needs_wasi = true,
474 WasmCompatibility::RequiresJsInterop => needs_js_interop = true,
475 WasmCompatibility::Incompatible => has_incompatible = true,
476 WasmCompatibility::Partial => {
477 needs_wasi = true;
479 }
480 WasmCompatibility::Full => {}
481 }
482 }
483
484 let fully_compatible = !needs_wasi && !needs_js_interop && !has_incompatible;
485
486 WasmCompatibilitySummary {
487 fully_compatible,
488 needs_wasi,
489 needs_js_interop,
490 has_incompatible,
491 modules_by_compat: modules_by_compat.clone(),
492 }
493 }
494
495 pub fn generate_cargo_toml_deps(&self, analysis: &ImportAnalysis) -> String {
497 let mut output = String::new();
498 output.push_str("[dependencies]\n");
499
500 for dep in &analysis.rust_dependencies {
501 if dep.target.is_none() {
502 if dep.features.is_empty() {
503 output.push_str(&format!("{} = \"{}\"\n", dep.crate_name, dep.version));
504 } else {
505 output.push_str(&format!(
506 "{} = {{ version = \"{}\", features = [{}] }}\n",
507 dep.crate_name,
508 dep.version,
509 dep.features.iter()
510 .map(|f| format!("\"{}\"", f))
511 .collect::<Vec<_>>()
512 .join(", ")
513 ));
514 }
515 }
516 }
517
518 output.push_str("\n[target.'cfg(target_arch = \"wasm32\")'.dependencies]\n");
520
521 if analysis.wasm_compatibility.needs_js_interop {
523 output.push_str("wasm-bindgen = \"0.2\"\n");
524 output.push_str("wasm-bindgen-futures = \"0.4\"\n");
525 output.push_str("js-sys = \"0.3\"\n");
526 }
527
528 if analysis.wasm_compatibility.needs_wasi {
530 output.push_str("\n[target.'cfg(target_arch = \"wasm32\")'.dependencies.wasi]\n");
531 output.push_str("version = \"0.11\"\n");
532 output.push_str("optional = true\n");
533 }
534
535 output
536 }
537
538 pub fn generate_compatibility_report(&self, analysis: &ImportAnalysis) -> String {
540 let mut report = String::new();
541
542 report.push_str("# WASM Compatibility Report\n\n");
543
544 if analysis.wasm_compatibility.fully_compatible {
545 report.push_str("✅ **Fully WASM Compatible** - No special requirements\n\n");
546 } else {
547 report.push_str("## Compatibility Status\n\n");
548
549 if analysis.wasm_compatibility.needs_wasi {
550 report.push_str("⚠️ **Requires WASI** - Needs filesystem/OS support\n");
551 }
552
553 if analysis.wasm_compatibility.needs_js_interop {
554 report.push_str("🌐 **Requires JS Interop** - Needs browser APIs\n");
555 }
556
557 if analysis.wasm_compatibility.has_incompatible {
558 report.push_str("❌ **Has Incompatible Modules** - Some features won't work in WASM\n");
559 }
560
561 report.push_str("\n");
562 }
563
564 report.push_str("## Module Compatibility\n\n");
566
567 let mut modules: Vec<_> = analysis.wasm_compatibility.modules_by_compat.iter().collect();
568 modules.sort_by_key(|(name, _)| *name);
569
570 for (module, compat) in modules {
571 let icon = match compat {
572 WasmCompatibility::Full => "✅",
573 WasmCompatibility::Partial => "🟡",
574 WasmCompatibility::RequiresWasi => "📁",
575 WasmCompatibility::RequiresJsInterop => "🌐",
576 WasmCompatibility::Incompatible => "❌",
577 };
578
579 report.push_str(&format!("{} `{}` - {:?}\n", icon, module, compat));
580 }
581
582 if !analysis.unmapped_modules.is_empty() {
584 report.push_str("\n## ⚠️ Unmapped Modules\n\n");
585 report.push_str("The following modules are not yet mapped to Rust crates:\n\n");
586
587 for module in &analysis.unmapped_modules {
588 report.push_str(&format!("- `{}`\n", module));
589 }
590
591 report.push_str("\nThese will need manual implementation or mapping.\n");
592 }
593
594 report
595 }
596}
597
598impl Default for ImportAnalyzer {
599 fn default() -> Self {
600 Self::new()
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn test_parse_simple_import() {
610 let analyzer = ImportAnalyzer::new();
611 let imports = analyzer.extract_imports("import json");
612
613 assert_eq!(imports.len(), 1);
614 assert_eq!(imports[0].module, "json");
615 assert_eq!(imports[0].import_type, ImportType::Module);
616 assert!(imports[0].items.is_empty());
617 }
618
619 #[test]
620 fn test_parse_import_with_alias() {
621 let analyzer = ImportAnalyzer::new();
622 let imports = analyzer.extract_imports("import datetime as dt");
623
624 assert_eq!(imports.len(), 1);
625 assert_eq!(imports[0].module, "datetime");
626 assert_eq!(imports[0].alias, Some("dt".to_string()));
627 }
628
629 #[test]
630 fn test_parse_from_import() {
631 let analyzer = ImportAnalyzer::new();
632 let imports = analyzer.extract_imports("from pathlib import Path");
633
634 assert_eq!(imports.len(), 1);
635 assert_eq!(imports[0].module, "pathlib");
636 assert_eq!(imports[0].items.len(), 1);
637 assert_eq!(imports[0].items[0].name, "Path");
638 assert_eq!(imports[0].items[0].alias, None);
639 assert_eq!(imports[0].import_type, ImportType::FromImport);
640 }
641
642 #[test]
643 fn test_parse_from_import_multiple() {
644 let analyzer = ImportAnalyzer::new();
645 let imports = analyzer.extract_imports("from datetime import datetime, timedelta");
646
647 assert_eq!(imports.len(), 1);
648 assert_eq!(imports[0].module, "datetime");
649 assert_eq!(imports[0].items.len(), 2);
650 assert_eq!(imports[0].items[0].name, "datetime");
651 assert_eq!(imports[0].items[1].name, "timedelta");
652 }
653
654 #[test]
655 fn test_parse_star_import() {
656 let analyzer = ImportAnalyzer::new();
657 let imports = analyzer.extract_imports("from os import *");
658
659 assert_eq!(imports.len(), 1);
660 assert_eq!(imports[0].module, "os");
661 assert_eq!(imports[0].import_type, ImportType::StarImport);
662 }
663
664 #[test]
665 fn test_analyze_mapped_modules() {
666 let analyzer = ImportAnalyzer::new();
667 let code = r#"
668import json
669from pathlib import Path
670import logging
671"#;
672
673 let analysis = analyzer.analyze(code);
674
675 assert_eq!(analysis.python_imports.len(), 3);
676 assert!(!analysis.rust_use_statements.is_empty());
677 assert!(analysis.unmapped_modules.is_empty());
678 }
679
680 #[test]
681 fn test_analyze_unmapped_module() {
682 let analyzer = ImportAnalyzer::new();
683 let code = "import nonexistent_module";
684
685 let analysis = analyzer.analyze(code);
686
687 assert_eq!(analysis.unmapped_modules.len(), 1);
688 assert_eq!(analysis.unmapped_modules[0], "nonexistent_module");
689 }
690
691 #[test]
692 fn test_wasm_compatibility_summary() {
693 let analyzer = ImportAnalyzer::new();
694 let code = r#"
695import json
696from pathlib import Path
697import asyncio
698"#;
699
700 let analysis = analyzer.analyze(code);
701
702 assert!(!analysis.wasm_compatibility.fully_compatible);
704 assert!(analysis.wasm_compatibility.needs_wasi);
705 assert!(analysis.wasm_compatibility.needs_js_interop);
706 }
707
708 #[test]
709 fn test_generate_cargo_toml() {
710 let analyzer = ImportAnalyzer::new();
711 let code = "import json\nfrom datetime import datetime";
712
713 let analysis = analyzer.analyze(code);
714 let cargo_toml = analyzer.generate_cargo_toml_deps(&analysis);
715
716 assert!(cargo_toml.contains("[dependencies]"));
717 }
718
719 #[test]
722 fn test_import_with_alias() {
723 let analyzer = ImportAnalyzer::new();
724 let imports = analyzer.extract_imports("import numpy as np");
725
726 assert_eq!(imports.len(), 1);
727 assert_eq!(imports[0].module, "numpy");
728 assert_eq!(imports[0].alias, Some("np".to_string()));
729 assert_eq!(imports[0].import_type, ImportType::Module);
730 }
731
732 #[test]
733 fn test_from_import_with_alias() {
734 let analyzer = ImportAnalyzer::new();
735 let imports = analyzer.extract_imports("from os import path as p");
736
737 assert_eq!(imports.len(), 1);
738 assert_eq!(imports[0].module, "os");
739 assert_eq!(imports[0].items.len(), 1);
740 assert_eq!(imports[0].items[0].name, "path");
741 assert_eq!(imports[0].items[0].alias, Some("p".to_string()));
742 }
743
744 #[test]
745 fn test_multiple_imports_one_line() {
746 let analyzer = ImportAnalyzer::new();
747 let imports = analyzer.extract_imports("import os, sys, json");
748
749 assert_eq!(imports.len(), 3);
750 assert_eq!(imports[0].module, "os");
751 assert_eq!(imports[1].module, "sys");
752 assert_eq!(imports[2].module, "json");
753 }
754
755 #[test]
756 fn test_from_import_multiple_with_aliases() {
757 let analyzer = ImportAnalyzer::new();
758 let imports = analyzer.extract_imports("from typing import List as L, Dict as D");
759
760 assert_eq!(imports.len(), 1);
761 assert_eq!(imports[0].module, "typing");
762 assert_eq!(imports[0].items.len(), 2);
763 assert_eq!(imports[0].items[0].name, "List");
764 assert_eq!(imports[0].items[0].alias, Some("L".to_string()));
765 assert_eq!(imports[0].items[1].name, "Dict");
766 assert_eq!(imports[0].items[1].alias, Some("D".to_string()));
767 }
768
769 #[test]
770 fn test_relative_import_single_dot() {
771 let analyzer = ImportAnalyzer::new();
772 let imports = analyzer.extract_imports("from . import utils");
773
774 assert_eq!(imports.len(), 1);
775 assert_eq!(imports[0].level, 1);
776 assert_eq!(imports[0].import_type, ImportType::FromImport);
777 }
778
779 #[test]
780 fn test_relative_import_double_dot() {
781 let analyzer = ImportAnalyzer::new();
782 let imports = analyzer.extract_imports("from .. import config");
783
784 assert_eq!(imports.len(), 1);
785 assert_eq!(imports[0].level, 2);
786 }
787
788 #[test]
789 fn test_relative_import_with_module() {
790 let analyzer = ImportAnalyzer::new();
791 let imports = analyzer.extract_imports("from .submodule import function");
792
793 assert_eq!(imports.len(), 1);
794 assert_eq!(imports[0].level, 1);
795 assert_eq!(imports[0].module, ".submodule");
796 assert_eq!(imports[0].items.len(), 1);
797 assert_eq!(imports[0].items[0].name, "function");
798 }
799
800 #[test]
801 fn test_resolve_relative_import_with_context() {
802 let mut analyzer = ImportAnalyzer::new();
803 analyzer.set_module_path("mypackage.subpackage.module".to_string());
804
805 let imports = analyzer.extract_imports("from . import utils");
806 assert_eq!(imports.len(), 1);
807 assert!(imports[0].module.contains("mypackage.subpackage") || imports[0].module == "");
809 }
810
811 #[test]
812 fn test_multiline_import() {
813 let analyzer = ImportAnalyzer::new();
814 let code = r#"
815from typing import (
816 List,
817 Dict,
818 Optional
819)
820"#;
821 let imports = analyzer.extract_imports(code);
822
823 assert_eq!(imports.len(), 1);
824 assert_eq!(imports[0].module, "typing");
825 assert_eq!(imports[0].items.len(), 3);
826 assert_eq!(imports[0].items[0].name, "List");
827 assert_eq!(imports[0].items[1].name, "Dict");
828 assert_eq!(imports[0].items[2].name, "Optional");
829 }
830
831 #[test]
832 fn test_import_location_tracking() {
833 let analyzer = ImportAnalyzer::new();
834 let code = r#"import os
835import sys
836from pathlib import Path"#;
837
838 let imports = analyzer.extract_imports(code);
839
840 assert_eq!(imports.len(), 3);
841 assert_eq!(imports[0].line, 0); assert_eq!(imports[1].line, 0);
847 assert_eq!(imports[2].line, 0);
848 }
849
850 #[test]
851 fn test_complex_import_scenario() {
852 let analyzer = ImportAnalyzer::new();
853 let code = r#"
854import os
855import sys as system
856from pathlib import Path
857from typing import List, Dict, Optional
858from collections import defaultdict as dd
859from . import utils
860from ..parent import config
861import json, re, random
862from os.path import join, exists as file_exists
863"#;
864
865 let imports = analyzer.extract_imports(code);
866
867 assert!(imports.len() >= 8);
869
870 let json_import = imports.iter().find(|i| i.module == "json");
872 assert!(json_import.is_some());
873
874 let sys_import = imports.iter().find(|i| i.module == "sys");
875 assert!(sys_import.is_some());
876 assert_eq!(sys_import.unwrap().alias, Some("system".to_string()));
877
878 let collections_import = imports.iter().find(|i| i.module == "collections");
880 assert!(collections_import.is_some());
881 if let Some(imp) = collections_import {
882 assert_eq!(imp.items.len(), 1);
883 assert_eq!(imp.items[0].name, "defaultdict");
884 assert_eq!(imp.items[0].alias, Some("dd".to_string()));
885 }
886 }
887
888 #[test]
889 fn test_error_handling_invalid_syntax() {
890 let analyzer = ImportAnalyzer::new();
891 let imports = analyzer.extract_imports("import this is not valid python");
893
894 assert_eq!(imports.len(), 0);
896 }
897
898 #[test]
899 fn test_submodule_import() {
900 let analyzer = ImportAnalyzer::new();
901 let imports = analyzer.extract_imports("from os.path import join");
902
903 assert_eq!(imports.len(), 1);
904 assert_eq!(imports[0].module, "os.path");
905 assert_eq!(imports[0].items.len(), 1);
906 assert_eq!(imports[0].items[0].name, "join");
907 }
908
909 #[test]
910 fn test_namespace_package_import() {
911 let analyzer = ImportAnalyzer::new();
912 let imports = analyzer.extract_imports("from xml.etree import ElementTree");
913
914 assert_eq!(imports.len(), 1);
915 assert_eq!(imports[0].module, "xml.etree");
916 assert_eq!(imports[0].items.len(), 1);
917 assert_eq!(imports[0].items[0].name, "ElementTree");
918 }
919}