neo_solidity/cli/cli_parts/cli_run/
imports.rs1use solang_parser::pt::{Import, ImportPath, SourceUnitPart};
2use std::collections::VecDeque;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone)]
6struct ResolvedSoliditySources {
7 files: Vec<(PathBuf, String)>,
8 combined_source: String,
9}
10
11fn resolve_solidity_sources_with_imports(
12 entry_file: &Path,
13 include_paths: &[PathBuf],
14) -> Result<ResolvedSoliditySources, String> {
15 let mut visited: HashSet<PathBuf> = HashSet::new();
16 let mut visiting: HashSet<PathBuf> = HashSet::new();
17 let mut ordered: Vec<(PathBuf, String)> = Vec::new();
18 let mut stack: VecDeque<PathBuf> = VecDeque::new();
19
20 fn extract_imports(source: &str, file: &Path) -> Result<Vec<String>, String> {
21 fn offset_to_line_column(source: &str, offset: usize) -> (usize, usize) {
22 let mut line = 1usize;
23 let mut column = 1usize;
24 let mut current = 0usize;
25
26 for ch in source.chars() {
27 if current >= offset {
28 break;
29 }
30
31 if ch == '\n' {
32 line += 1;
33 column = 1;
34 } else {
35 column += 1;
36 }
37
38 current += ch.len_utf8();
39 }
40
41 (line, column)
42 }
43
44 let (unit, _comments) = solang_parser::parse(source, 0).map_err(|diags| {
45 let summary = diags
46 .iter()
47 .map(|diag| {
48 if let solang_parser::pt::Loc::File(_, start, _) = diag.loc {
49 let (line, column) = offset_to_line_column(source, start);
50 format!("{}:{}: {}", line, column, diag.message)
51 } else {
52 diag.message.clone()
53 }
54 })
55 .collect::<Vec<_>>()
56 .join("\n");
57 format!(
58 "failed to parse '{}' while resolving imports:\n{}",
59 file.display(),
60 summary
61 )
62 })?;
63
64 let mut imports = Vec::new();
65 for part in unit.0.iter() {
66 let SourceUnitPart::ImportDirective(import) = part else {
67 continue;
68 };
69
70 match import {
71 Import::Plain(path, _) => {
72 imports.push(extract_import_path_string(path, file)?);
73 }
74 Import::Rename(path, _renames, _) => {
75 imports.push(extract_import_path_string(path, file)?);
76 }
77 Import::GlobalSymbol(path, _, _) => {
78 imports.push(extract_import_path_string(path, file)?);
79 }
80 }
81 }
82
83 Ok(imports)
84 }
85
86 fn extract_import_path_string(path: &ImportPath, file: &Path) -> Result<String, String> {
87 match path {
88 ImportPath::Filename(lit) => Ok(lit.string.clone()),
89 ImportPath::Path(_) => Err(format!(
90 "unsupported import path kind in '{}': path imports are not supported",
91 file.display()
92 )),
93 }
94 }
95
96 fn resolve_import_file(
97 import_path: &str,
98 from_file: &Path,
99 include_paths: &[PathBuf],
100 ) -> Result<PathBuf, String> {
101 let import = Path::new(import_path);
102
103 let mut candidates: Vec<PathBuf> = Vec::new();
104 if import.is_absolute() {
105 candidates.push(import.to_path_buf());
106 } else {
107 let from_dir = from_file.parent().unwrap_or_else(|| Path::new("."));
108 candidates.push(from_dir.join(import));
109 for include_dir in include_paths {
110 candidates.push(include_dir.join(import));
111 }
112 candidates.push(import.to_path_buf());
113 }
114
115 for candidate in candidates {
116 if candidate.exists() {
117 return Ok(candidate.canonicalize().unwrap_or(candidate));
118 }
119 }
120
121 Err(format!(
122 "failed to resolve import '{import_path}' from '{}'",
123 from_file.display()
124 ))
125 }
126
127 fn visit_file(
128 file: &Path,
129 include_paths: &[PathBuf],
130 visited: &mut HashSet<PathBuf>,
131 visiting: &mut HashSet<PathBuf>,
132 ordered: &mut Vec<(PathBuf, String)>,
133 stack: &mut VecDeque<PathBuf>,
134 ) -> Result<(), String> {
135 let canonical = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
136
137 if visited.contains(&canonical) {
138 return Ok(());
139 }
140
141 if !visiting.insert(canonical.clone()) {
142 let mut chain = stack
143 .iter()
144 .map(|p| p.display().to_string())
145 .collect::<Vec<_>>();
146 chain.push(canonical.display().to_string());
147 return Err(format!("import cycle detected: {}", chain.join(" -> ")));
148 }
149
150 stack.push_back(canonical.clone());
151 let content = fs::read_to_string(&canonical)
152 .map_err(|err| format!("failed to read '{}': {err}", canonical.display()))?;
153
154 let imports = extract_imports(&content, &canonical)?;
155 for import in imports {
156 let resolved = resolve_import_file(&import, &canonical, include_paths)?;
157 visit_file(
158 &resolved,
159 include_paths,
160 visited,
161 visiting,
162 ordered,
163 stack,
164 )?;
165 }
166
167 stack.pop_back();
168 visiting.remove(&canonical);
169 visited.insert(canonical.clone());
170 ordered.push((canonical, content));
171 Ok(())
172 }
173
174 visit_file(
175 entry_file,
176 include_paths,
177 &mut visited,
178 &mut visiting,
179 &mut ordered,
180 &mut stack,
181 )?;
182
183 let mut combined = String::new();
184 for (idx, (path, content)) in ordered.iter().enumerate() {
185 if idx > 0 {
186 combined.push_str("\n\n");
187 }
188 combined.push_str(&format!("// --- {}\n", path.display()));
189 combined.push_str(content);
190 }
191
192 Ok(ResolvedSoliditySources {
193 files: ordered,
194 combined_source: combined,
195 })
196}