1use eyre::Result;
2use foundry_compilers::{
3 CompilerInput, Graph, Project, ProjectCompileOutput, ProjectPathsConfig,
4 artifacts::{Source, Sources},
5 multi::{MultiCompilerLanguage, MultiCompilerParser},
6 solc::{SOLC_EXTENSIONS, SolcLanguage, SolcVersionedInput},
7};
8use foundry_config::Config;
9#[cfg(windows)]
10use path_slash::PathExt as _;
11use rayon::prelude::*;
12use solar::{interface::MIN_SOLIDITY_VERSION, sema::ParsingContext};
13#[cfg(windows)]
14use std::os::windows::ffi::OsStrExt as _;
15use std::{
16 collections::{HashSet, VecDeque},
17 path::{Path, PathBuf},
18};
19
20pub fn configure_pcx(
28 pcx: &mut ParsingContext<'_>,
29 config: &Config,
30 project: Option<&Project>,
31 target_paths: Option<&[PathBuf]>,
32) -> Result<()> {
33 let status = configure_pcx_with_sources(pcx, config, project, target_paths, false)?;
34 if !status.has_compatible_sources && !status.has_unsupported_sources {
35 eyre::bail!("no Solidity sources");
36 }
37 Ok(())
38}
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub struct SolarSourceStatus {
43 has_compatible_sources: bool,
45 has_unsupported_sources: bool,
47}
48
49impl SolarSourceStatus {
50 pub const fn is_fully_supported(&self) -> bool {
52 self.has_compatible_sources && !self.has_unsupported_sources
53 }
54}
55
56pub fn configure_pcx_all_sources(
60 pcx: &mut ParsingContext<'_>,
61 config: &Config,
62 project: Option<&Project>,
63 target_paths: Option<&[PathBuf]>,
64) -> Result<bool> {
65 let status = configure_pcx_all_sources_with_status(pcx, config, project, target_paths)?;
66 if !status.has_compatible_sources && !status.has_unsupported_sources {
67 eyre::bail!("no Solidity sources");
68 }
69 Ok(status.has_compatible_sources)
70}
71
72pub fn configure_pcx_all_sources_with_status(
75 pcx: &mut ParsingContext<'_>,
76 config: &Config,
77 project: Option<&Project>,
78 target_paths: Option<&[PathBuf]>,
79) -> Result<SolarSourceStatus> {
80 configure_pcx_with_sources(pcx, config, project, target_paths, true)
81}
82
83fn configure_pcx_with_sources(
84 pcx: &mut ParsingContext<'_>,
85 config: &Config,
86 project: Option<&Project>,
87 target_paths: Option<&[PathBuf]>,
88 all_versions: bool,
89) -> Result<SolarSourceStatus> {
90 let project = match project {
92 Some(project) => project,
93 None => &config.ephemeral_project()?,
94 };
95
96 let sources = match target_paths {
97 Some(targets) => {
99 let mut sources = Sources::new();
100 for t in targets {
101 let path = dunce::canonicalize(t)?;
102 let source = Source::read(&path)?;
103 sources.insert(path, source);
104 }
105 sources
106 }
107 None => {
109 let mut sources = project.paths.read_input_files()?;
110 if let Some(filter) = &project.sparse_output {
111 sources.retain(|path, _| filter.is_match(path));
112 }
113 sources
114 }
115 };
116
117 let graph = Graph::<MultiCompilerParser>::resolve_sources(&project.paths, sources)?;
119 let Some(versioned_sources) = graph
120 .into_sources_by_version(project)?
122 .sources
123 .into_iter()
124 .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity))
126 .map(|(_, sources)| sources)
127 else {
128 return Ok(SolarSourceStatus::default());
129 };
130
131 let mut has_unsupported_sources = false;
132 let versioned_sources = versioned_sources.into_iter().filter(|(version, sources, _)| {
133 if version < &MIN_SOLIDITY_VERSION && !sources.is_empty() {
134 has_unsupported_sources = true;
135 false
136 } else {
137 true
138 }
139 });
140 let (version, sources) = if all_versions {
141 versioned_sources.fold(
142 (MIN_SOLIDITY_VERSION, Sources::default()),
143 |(version, mut sources), (source_version, version_sources, _)| {
144 sources.extend(version_sources);
145 (version.max(source_version), sources)
146 },
147 )
148 } else {
149 versioned_sources
150 .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2))
151 .map_or((MIN_SOLIDITY_VERSION, Sources::default()), |(v, s, _)| (v, s))
152 };
153
154 let has_sources = !sources.is_empty();
155 if !all_versions && !has_sources {
156 sh_warn!("no files found. Solar doesn't support Solidity versions prior to 0.8.0")?;
157 }
158
159 let solc = SolcVersionedInput::build(
160 sources,
161 config.solc_settings()?,
162 SolcLanguage::Solidity,
163 version,
164 );
165
166 configure_pcx_from_solc(pcx, &project.paths, &solc, true);
167
168 Ok(SolarSourceStatus { has_compatible_sources: has_sources, has_unsupported_sources })
169}
170
171pub fn get_solar_sources_from_compile_output(
178 config: &Config,
179 output: &ProjectCompileOutput,
180 target_paths: Option<&[PathBuf]>,
181 ignored_paths: Option<&[PathBuf]>,
182) -> Result<SolcVersionedInput> {
183 let is_solidity_file = |path: &Path| -> bool {
184 path.extension().and_then(|s| s.to_str()).is_some_and(|ext| SOLC_EXTENSIONS.contains(&ext))
185 };
186
187 let is_ignored = |path: &Path| -> bool {
188 if let Some(ignored) = ignored_paths {
189 ignored.iter().any(|ignored_path| path == ignored_path)
190 } else {
191 false
192 }
193 };
194
195 let mut source_paths: HashSet<PathBuf> = if let Some(targets) = target_paths
197 && !targets.is_empty()
198 {
199 let mut source_paths = HashSet::new();
200 let mut queue: VecDeque<PathBuf> = targets
201 .iter()
202 .filter_map(|path| {
203 is_solidity_file(path).then(|| dunce::canonicalize(path).ok()).flatten()
204 })
205 .collect();
206
207 while let Some(path) = queue.pop_front() {
208 if source_paths.insert(path.clone()) {
209 for import in output.graph().imports(path.as_path()) {
210 if !is_ignored(import) {
212 queue.push_back(import.to_path_buf());
213 }
214 }
215 }
216 }
217
218 source_paths
219 } else {
220 output
221 .graph()
222 .source_files()
223 .filter_map(|idx| {
224 let path = output.graph().node_path(idx).to_path_buf();
225 is_solidity_file(&path).then_some(path)
226 })
227 .collect()
228 };
229
230 let (version, sources) = {
232 let (mut max_version, mut sources) = (MIN_SOLIDITY_VERSION, Sources::new());
233 for (id, _) in output.artifact_ids() {
234 if let Ok(path) = dunce::canonicalize(&id.source)
235 && source_paths.remove(&path)
236 {
237 if id.version < MIN_SOLIDITY_VERSION {
238 continue;
239 } else if max_version < id.version {
240 max_version = id.version;
241 };
242
243 let source = Source::read(&path)?;
244 sources.insert(path, source);
245 }
246 }
247
248 (max_version, sources)
249 };
250
251 let solc = SolcVersionedInput::build(
252 sources,
253 config.solc_settings()?,
254 SolcLanguage::Solidity,
255 version,
256 );
257
258 Ok(solc)
259}
260
261pub fn configure_pcx_from_compile_output(
263 pcx: &mut ParsingContext<'_>,
264 config: &Config,
265 output: &ProjectCompileOutput,
266 target_paths: Option<&[PathBuf]>,
267) -> Result<()> {
268 let solc = get_solar_sources_from_compile_output(config, output, target_paths, None)?;
269 configure_pcx_from_solc(pcx, &config.project_paths(), &solc, true);
270 Ok(())
271}
272
273#[cfg(windows)]
276fn solar_slash_path(path: &Path) -> String {
277 let has_trailing_separator = path
278 .as_os_str()
279 .encode_wide()
280 .last()
281 .is_some_and(|c| c == u16::from(b'/') || c == u16::from(b'\\'));
282 let mut path = path.to_slash_lossy().into_owned();
283 if has_trailing_separator && !path.ends_with('/') {
284 path.push('/');
285 }
286 path
287}
288
289pub fn configure_pcx_from_solc(
294 pcx: &mut ParsingContext<'_>,
295 project_paths: &ProjectPathsConfig,
296 vinput: &SolcVersionedInput,
297 add_source_files: bool,
298) {
299 configure_pcx_from_solc_cli(pcx, project_paths, &vinput.cli_settings);
300 if add_source_files {
301 let sources = vinput
302 .input
303 .sources
304 .par_iter()
305 .filter_map(|(path, source)| {
306 #[cfg(windows)]
307 let path = PathBuf::from(solar_slash_path(path));
308 #[cfg(not(windows))]
309 let path = path.clone();
310 pcx.sess.source_map().new_source_file(path, source.content.as_str()).ok()
311 })
312 .collect::<Vec<_>>();
313 pcx.add_files(sources);
314 }
315}
316
317fn configure_pcx_from_solc_cli(
318 pcx: &mut ParsingContext<'_>,
319 project_paths: &ProjectPathsConfig,
320 cli_settings: &foundry_compilers::solc::CliSettings,
321) {
322 pcx.file_resolver
323 .set_current_dir(cli_settings.base_path.as_ref().unwrap_or(&project_paths.root));
324 for remapping in &project_paths.remappings {
325 let context = remapping.context.clone().unwrap_or_default();
326 #[cfg(windows)]
329 let context = solar_slash_path(Path::new(&context));
330 pcx.file_resolver.add_import_remapping(solar::sema::interface::config::ImportRemapping {
331 context,
332 prefix: remapping.name.clone(),
333 path: remapping.path.clone(),
334 });
335 }
336 pcx.file_resolver.add_include_paths(cli_settings.include_paths.iter().cloned());
337}
338
339#[cfg(all(test, windows))]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn solar_slash_path_preserves_windows_prefixes_and_boundaries() {
345 for (path, expected) in [
346 (r"lib\outer\", "lib/outer/"),
347 (r"\\server\share\project/", r"\\server\share/project/"),
348 (r"\\?\C:\project/", r"\\?\C:/project/"),
349 (r"\\?\UNC\server\share\project/", r"\\?\UNC\server\share/project/"),
350 ] {
351 assert_eq!(solar_slash_path(Path::new(path)), expected);
352 }
353 }
354}