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