1use super::{
2 CompilationError, Compiler, CompilerInput, CompilerOutput, CompilerSettings, CompilerVersion,
3 Language, ParsedSource, restrictions::CompilerSettingsRestrictions,
4};
5use crate::{
6 SourceParser,
7 resolver::{
8 Node,
9 parse::{SolData, SolParser},
10 },
11};
12use foundry_compilers_artifacts::{
13 BytecodeHash, Contract, Error, EvmVersion, Settings, Severity, SolcInput,
14 error::SourceLocation,
15 output_selection::OutputSelection,
16 remappings::Remapping,
17 sources::{Source, Sources},
18};
19use foundry_compilers_core::error::{Result, SolcError, SolcIoError};
20use rayon::prelude::*;
21use semver::Version;
22use serde::{Deserialize, Serialize};
23use std::{
24 borrow::Cow,
25 collections::{BTreeMap, BTreeSet},
26 ops::{Deref, DerefMut},
27 path::{Path, PathBuf},
28};
29
30pub use foundry_compilers_artifacts::SolcLanguage;
31
32mod compiler;
33pub use compiler::{SOLC_EXTENSIONS, Solc};
34
35#[derive(Clone, Debug)]
36#[cfg_attr(feature = "svm-solc", derive(Default))]
37pub enum SolcCompiler {
38 #[default]
39 #[cfg(feature = "svm-solc")]
40 AutoDetect,
41
42 Specific(Solc),
43}
44
45impl Language for SolcLanguage {
46 const FILE_EXTENSIONS: &'static [&'static str] = SOLC_EXTENSIONS;
47}
48
49impl Compiler for SolcCompiler {
50 type Input = SolcVersionedInput;
51 type CompilationError = Error;
52 type Parser = SolParser;
53 type Settings = SolcSettings;
54 type Language = SolcLanguage;
55 type CompilerContract = Contract;
56
57 fn compile(
58 &self,
59 input: &Self::Input,
60 ) -> Result<CompilerOutput<Self::CompilationError, Self::CompilerContract>> {
61 let mut solc = match self {
62 Self::Specific(solc) => solc.clone(),
63
64 #[cfg(feature = "svm-solc")]
65 Self::AutoDetect => Solc::find_or_install(&input.version)?,
66 };
67 solc.base_path.clone_from(&input.cli_settings.base_path);
68 solc.allow_paths.clone_from(&input.cli_settings.allow_paths);
69 solc.include_paths.clone_from(&input.cli_settings.include_paths);
70 solc.extra_args.extend_from_slice(&input.cli_settings.extra_args);
71
72 let solc_output = solc.compile(&input.input)?;
73
74 let output = CompilerOutput {
75 errors: solc_output.errors,
76 contracts: solc_output.contracts,
77 sources: solc_output.sources,
78 metadata: BTreeMap::new(),
79 };
80
81 Ok(output)
82 }
83
84 fn available_versions(&self, _language: &Self::Language) -> Vec<CompilerVersion> {
85 match self {
86 Self::Specific(solc) => vec![CompilerVersion::Installed(Version::new(
87 solc.version.major,
88 solc.version.minor,
89 solc.version.patch,
90 ))],
91
92 #[cfg(feature = "svm-solc")]
93 Self::AutoDetect => {
94 let mut all_versions = Solc::installed_versions()
95 .into_iter()
96 .map(CompilerVersion::Installed)
97 .collect::<Vec<_>>();
98 let mut uniques = all_versions
99 .iter()
100 .map(|v| {
101 let v = v.as_ref();
102 (v.major, v.minor, v.patch)
103 })
104 .collect::<std::collections::HashSet<_>>();
105 all_versions.extend(
106 Solc::released_versions()
107 .into_iter()
108 .filter(|v| uniques.insert((v.major, v.minor, v.patch)))
109 .map(CompilerVersion::Remote),
110 );
111 all_versions.sort_unstable();
112 all_versions
113 }
114 }
115 }
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct SolcVersionedInput {
121 pub version: Version,
122 #[serde(flatten)]
123 pub input: SolcInput,
124 #[serde(flatten)]
125 pub cli_settings: CliSettings,
126}
127
128impl CompilerInput for SolcVersionedInput {
129 type Settings = SolcSettings;
130 type Language = SolcLanguage;
131
132 fn build(
137 sources: Sources,
138 settings: Self::Settings,
139 language: Self::Language,
140 version: Version,
141 ) -> Self {
142 let SolcSettings { settings, cli_settings } = settings;
143 let input = SolcInput::new(language, sources, settings).sanitized(&version);
144
145 Self { version, input, cli_settings }
146 }
147
148 fn language(&self) -> Self::Language {
149 self.input.language
150 }
151
152 fn version(&self) -> &Version {
153 &self.version
154 }
155
156 fn sources(&self) -> impl Iterator<Item = (&Path, &Source)> {
157 self.input.sources.iter().map(|(path, source)| (path.as_path(), source))
158 }
159
160 fn compiler_name(&self) -> Cow<'static, str> {
161 if self.version.build.as_str().contains("solar") { "Solar".into() } else { "Solc".into() }
163 }
164
165 fn strip_prefix(&mut self, base: &Path) {
166 self.input.strip_prefix(base);
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
171#[serde(rename_all = "camelCase")]
172pub struct CliSettings {
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub extra_args: Vec<String>,
175 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
176 pub allow_paths: BTreeSet<PathBuf>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub base_path: Option<PathBuf>,
179 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
180 pub include_paths: BTreeSet<PathBuf>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
184pub struct SolcSettings {
185 #[serde(flatten)]
187 pub settings: Settings,
188 #[serde(flatten)]
190 pub cli_settings: CliSettings,
191}
192
193impl Deref for SolcSettings {
194 type Target = Settings;
195
196 fn deref(&self) -> &Self::Target {
197 &self.settings
198 }
199}
200
201impl DerefMut for SolcSettings {
202 fn deref_mut(&mut self) -> &mut Self::Target {
203 &mut self.settings
204 }
205}
206
207#[derive(Debug, Clone, Copy, Eq, Default, PartialEq)]
209pub struct Restriction<V> {
210 pub min: Option<V>,
211 pub max: Option<V>,
212}
213
214impl<V: Ord + Copy> Restriction<V> {
215 pub fn satisfies(&self, value: Option<V>) -> bool {
219 self.min.is_none_or(|min| value.is_some_and(|v| v >= min))
220 && self.max.is_none_or(|max| value.is_some_and(|v| v <= max))
221 }
222
223 pub fn merge(self, other: Self) -> Option<Self> {
225 let Self { mut min, mut max } = self;
226 let Self { min: other_min, max: other_max } = other;
227
228 min = min.map_or(other_min, |this_min| {
229 Some(other_min.map_or(this_min, |other_min| this_min.max(other_min)))
230 });
231 max = max.map_or(other_max, |this_max| {
232 Some(other_max.map_or(this_max, |other_max| this_max.min(other_max)))
233 });
234
235 if let (Some(min), Some(max)) = (min, max)
236 && min > max
237 {
238 return None;
239 }
240
241 Some(Self { min, max })
242 }
243
244 pub fn apply(&self, value: Option<V>) -> Option<V> {
245 match (value, self.min, self.max) {
246 (None, Some(min), _) => Some(min),
247 (None, None, Some(max)) => Some(max),
248 (Some(cur), Some(min), _) if cur < min => Some(min),
249 (Some(cur), _, Some(max)) if cur > max => Some(max),
250 _ => value,
251 }
252 }
253}
254
255#[derive(Debug, Clone, Copy, Default)]
257pub struct SolcRestrictions {
258 pub evm_version: Restriction<EvmVersion>,
259 pub via_ir: Option<bool>,
260 pub optimizer_runs: Restriction<usize>,
261 pub bytecode_hash: Option<BytecodeHash>,
262}
263
264impl CompilerSettingsRestrictions for SolcRestrictions {
265 fn merge(self, other: Self) -> Option<Self> {
266 if let (Some(via_ir), Some(other_via_ir)) = (self.via_ir, other.via_ir)
267 && via_ir != other_via_ir
268 {
269 return None;
270 }
271
272 if let (Some(bytecode_hash), Some(other_bytecode_hash)) =
273 (self.bytecode_hash, other.bytecode_hash)
274 && bytecode_hash != other_bytecode_hash
275 {
276 return None;
277 }
278
279 Some(Self {
280 evm_version: self.evm_version.merge(other.evm_version)?,
281 via_ir: self.via_ir.or(other.via_ir),
282 optimizer_runs: self.optimizer_runs.merge(other.optimizer_runs)?,
283 bytecode_hash: self.bytecode_hash.or(other.bytecode_hash),
284 })
285 }
286}
287
288impl CompilerSettings for SolcSettings {
289 type Restrictions = SolcRestrictions;
290
291 fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) {
292 f(&mut self.settings.output_selection);
293 }
294
295 fn can_use_cached(&self, other: &Self) -> bool {
296 let Self {
297 settings:
298 Settings {
299 stop_after,
300 remappings,
301 optimizer,
302 model_checker,
303 metadata,
304 output_selection,
305 evm_version,
306 via_ir,
307 debug,
308 libraries,
309 },
310 ..
311 } = self;
312
313 *stop_after == other.settings.stop_after
314 && *remappings == other.settings.remappings
315 && *optimizer == other.settings.optimizer
316 && *model_checker == other.settings.model_checker
317 && *metadata == other.settings.metadata
318 && *evm_version == other.settings.evm_version
319 && *via_ir == other.settings.via_ir
320 && *debug == other.settings.debug
321 && *libraries == other.settings.libraries
322 && output_selection.is_subset_of(&other.settings.output_selection)
323 }
324
325 fn with_remappings(mut self, remappings: &[Remapping]) -> Self {
326 self.settings.remappings = remappings.to_vec();
327
328 self
329 }
330
331 fn with_allow_paths(mut self, allowed_paths: &BTreeSet<PathBuf>) -> Self {
332 self.cli_settings.allow_paths.clone_from(allowed_paths);
333 self
334 }
335
336 fn with_base_path(mut self, base_path: &Path) -> Self {
337 self.cli_settings.base_path = Some(base_path.to_path_buf());
338 self
339 }
340
341 fn with_include_paths(mut self, include_paths: &BTreeSet<PathBuf>) -> Self {
342 self.cli_settings.include_paths.clone_from(include_paths);
343 self
344 }
345
346 fn satisfies_restrictions(&self, restrictions: &Self::Restrictions) -> bool {
347 let mut satisfies = true;
348
349 let SolcRestrictions { evm_version, via_ir, optimizer_runs, bytecode_hash } = restrictions;
350
351 satisfies &= evm_version.satisfies(self.evm_version);
352 satisfies &= via_ir.is_none_or(|via_ir| via_ir == self.via_ir.unwrap_or_default());
353 satisfies &= bytecode_hash.is_none_or(|bytecode_hash| {
354 self.metadata.as_ref().and_then(|m| m.bytecode_hash) == Some(bytecode_hash)
355 });
356 satisfies &= optimizer_runs.satisfies(self.optimizer.runs);
357
358 satisfies &= optimizer_runs
360 .min
361 .is_none_or(|min| min == 0 || self.optimizer.enabled.unwrap_or_default());
362
363 satisfies
364 }
365}
366
367impl SourceParser for SolParser {
368 type ParsedSource = SolData;
369
370 fn new(config: &crate::ProjectPathsConfig) -> Self {
371 Self {
372 compiler: solar::sema::Compiler::new(Self::session_with_opts(
373 solar::sema::interface::config::Opts {
374 include_paths: config.include_paths.iter().cloned().collect(),
375 base_path: Some(config.root.clone()),
376 import_remappings: config
377 .remappings
378 .iter()
379 .map(|r| solar::sema::interface::config::ImportRemapping {
380 context: r.context.clone().unwrap_or_default(),
381 prefix: r.name.clone(),
382 path: r.path.clone(),
383 })
384 .collect(),
385 ..Default::default()
386 },
387 )),
388 }
389 }
390
391 fn read(&mut self, path: &Path) -> Result<Node<Self::ParsedSource>> {
392 let mut sources = Sources::from_iter([(path.to_path_buf(), Source::read_(path)?)]);
393 let nodes = self.parse_sources(&mut sources)?;
394 debug_assert_eq!(nodes.len(), 1, "{nodes:#?}");
395 Ok(nodes.into_iter().next().unwrap().1)
396 }
397
398 fn parse_sources(
399 &mut self,
400 sources: &mut Sources,
401 ) -> Result<Vec<(PathBuf, Node<Self::ParsedSource>)>> {
402 self.compiler.enter_mut(|compiler| {
403 let mut pcx = compiler.parse();
404 pcx.set_resolve_imports(false);
405 let files = sources
406 .par_iter()
407 .map(|(path, source)| {
408 pcx.sess
409 .source_map()
410 .new_source_file(path.clone(), source.content.as_str())
411 .map_err(|e| SolcError::Io(SolcIoError::new(e, path)))
412 })
413 .collect::<Result<Vec<_>>>()?;
414 pcx.add_files(files);
415 pcx.parse();
416
417 let parsed = sources.par_iter().map(|(path, source)| {
418 let sf = compiler.sess().source_map().get_file(path).unwrap();
419 let (_, s) = compiler.gcx().sources.get_file(&sf).unwrap();
420 let node = Node::new(
421 path.clone(),
422 source.clone(),
423 SolData::parse_from(compiler.gcx().sess, s),
424 );
425 (path.clone(), node)
426 });
427 let parsed = parsed.collect::<Vec<_>>();
428
429 Ok(parsed)
430 })
431 }
432
433 fn finalize_imports(
434 &mut self,
435 nodes: &mut Vec<Node<Self::ParsedSource>>,
436 include_paths: &BTreeSet<PathBuf>,
437 ) -> Result<()> {
438 let compiler = &mut self.compiler;
439 compiler.sess_mut().opts.include_paths.extend(include_paths.iter().cloned());
440 compiler.enter_mut(|compiler| {
441 let mut pcx = compiler.parse();
442 pcx.set_resolve_imports(true);
443 pcx.force_resolve_all_imports();
444 });
445
446 if let Some(Err(diag)) = compiler.sess().emitted_errors()
449 && let Some(idx) = nodes
450 .iter()
451 .position(|node| node.data.parse_result.is_ok())
452 .or_else(|| nodes.first().map(|_| 0))
453 {
454 nodes[idx].data.parse_result = Err(diag.to_string());
455 }
456
457 for node in nodes.iter() {
458 if let Err(e) = &node.data.parse_result {
459 debug!("failed parsing:\n{e}");
460 }
461 }
462
463 Ok(())
464 }
465}
466
467impl ParsedSource for SolData {
468 type Language = SolcLanguage;
469
470 fn parse(content: &str, file: &std::path::Path) -> Result<Self> {
471 Ok(Self::parse(content, file))
472 }
473
474 fn version_req(&self) -> Option<&semver::VersionReq> {
475 self.version_req.as_ref()
476 }
477
478 fn contract_names(&self) -> &[String] {
479 &self.contract_names
480 }
481
482 fn language(&self) -> Self::Language {
483 if self.is_yul { SolcLanguage::Yul } else { SolcLanguage::Solidity }
484 }
485
486 fn resolve_imports<C>(
487 &self,
488 _paths: &crate::ProjectPathsConfig<C>,
489 _include_paths: &mut BTreeSet<PathBuf>,
490 ) -> Result<Vec<PathBuf>> {
491 Ok(self.imports.iter().map(|i| i.data().path().to_path_buf()).collect())
492 }
493
494 fn compilation_dependencies<'a>(
495 &self,
496 imported_nodes: impl Iterator<Item = (&'a Path, &'a Self)>,
497 ) -> impl Iterator<Item = &'a Path>
498 where
499 Self: 'a,
500 {
501 imported_nodes.filter_map(|(path, node)| (!node.libraries.is_empty()).then_some(path))
502 }
503}
504
505impl CompilationError for Error {
506 fn is_warning(&self) -> bool {
507 self.severity.is_warning()
508 }
509 fn is_error(&self) -> bool {
510 self.severity.is_error()
511 }
512
513 fn source_location(&self) -> Option<SourceLocation> {
514 self.source_location.clone()
515 }
516
517 fn severity(&self) -> Severity {
518 self.severity
519 }
520
521 fn error_code(&self) -> Option<u64> {
522 self.error_code
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use foundry_compilers_artifacts::{CompilerOutput, SolcLanguage};
529 use semver::Version;
530
531 use crate::{
532 AggregatedCompilerOutput,
533 buildinfo::RawBuildInfo,
534 compilers::{
535 CompilerInput,
536 solc::{SolcCompiler, SolcVersionedInput},
537 },
538 };
539
540 #[test]
541 fn can_parse_declaration_error() {
542 let s = r#"{
543 "errors": [
544 {
545 "component": "general",
546 "errorCode": "7576",
547 "formattedMessage": "DeclarationError: Undeclared identifier. Did you mean \"revert\"?\n --> /Users/src/utils/UpgradeProxy.sol:35:17:\n |\n35 | revert(\"Transparent ERC1967 proxies do not have upgradeable implementations\");\n | ^^^^^^\n\n",
548 "message": "Undeclared identifier. Did you mean \"revert\"?",
549 "severity": "error",
550 "sourceLocation": {
551 "end": 1623,
552 "file": "/Users/src/utils/UpgradeProxy.sol",
553 "start": 1617
554 },
555 "type": "DeclarationError"
556 }
557 ],
558 "sources": { }
559}"#;
560
561 let out: CompilerOutput = serde_json::from_str(s).unwrap();
562 assert_eq!(out.errors.len(), 1);
563
564 let out_converted = crate::compilers::CompilerOutput {
565 errors: out.errors,
566 contracts: Default::default(),
567 sources: Default::default(),
568 metadata: Default::default(),
569 };
570
571 let v = Version::new(0, 8, 12);
572 let input = SolcVersionedInput::build(
573 Default::default(),
574 Default::default(),
575 SolcLanguage::Solidity,
576 v.clone(),
577 );
578 let build_info = RawBuildInfo::new(&input, &out_converted, true).unwrap();
579 let mut aggregated = AggregatedCompilerOutput::<SolcCompiler>::default();
580 aggregated.extend(v, build_info, "default", out_converted);
581 assert!(!aggregated.is_unchanged());
582 }
583
584 #[test]
585 fn test_compiler_name_detection() {
586 use std::str::FromStr;
587
588 let solc_version = Version::from_str("0.8.28+commit.2d360a2").unwrap();
590 let input = SolcVersionedInput::build(
591 Default::default(),
592 Default::default(),
593 SolcLanguage::Solidity,
594 solc_version,
595 );
596 assert_eq!(input.compiler_name().as_ref(), "Solc");
597
598 let solar_version = Version::from_str("0.8.28+commit.2d360a2.solar.0.1.8").unwrap();
600 let input = SolcVersionedInput::build(
601 Default::default(),
602 Default::default(),
603 SolcLanguage::Solidity,
604 solar_version,
605 );
606 assert_eq!(input.compiler_name().as_ref(), "Solar");
607 }
608}