neo_solidity/cli/cli_parts/cli_run/
imports.rs

1use 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}