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