Skip to main content

foundry_compilers/compilers/solc/
mod.rs

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    /// Creates a new [CompilerInput]s with default settings and the given sources
133    ///
134    /// A [CompilerInput] expects a language setting, supported by solc are solidity or yul.
135    /// In case the `sources` is a mix of solidity and yul files, 2 CompilerInputs are returned
136    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        // Detect Solar from version build metadata (e.g., "0.8.28+commit.xxx.solar.0.1.8")
162        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    /// JSON settings expected by Solc
186    #[serde(flatten)]
187    pub settings: Settings,
188    /// Additional CLI args configuration
189    #[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/// Abstraction over min/max restrictions on some value.
208#[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    /// Returns true if the given value satisfies the restrictions
216    ///
217    /// If given None, only returns true if no restrictions are set
218    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    /// Combines two restrictions into a new one
224    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/// Restrictions on settings for the solc compiler.
256#[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        // Ensure that we either don't have min optimizer runs set or that the optimizer is enabled
359        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        // Set error on the first successful source, if any. This doesn't really have to be
447        // exact, as long as at least one source has an error set it should be enough.
448        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        // Regular solc version
589        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        // Solar version (contains "solar" in build metadata)
599        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}