foundry_compilers/compilers/solc/
mod.rs

1use super::{
2    restrictions::CompilerSettingsRestrictions, CompilationError, Compiler, CompilerInput,
3    CompilerOutput, CompilerSettings, CompilerVersion, Language, ParsedSource,
4};
5use crate::{
6    resolver::{
7        parse::{SolData, SolParser},
8        Node,
9    },
10    SourceParser,
11};
12use foundry_compilers_artifacts::{
13    error::SourceLocation,
14    output_selection::OutputSelection,
15    remappings::Remapping,
16    sources::{Source, Sources},
17    BytecodeHash, Contract, Error, EvmVersion, Settings, Severity, SolcInput,
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, SOLC_EXTENSIONS};
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        "Solc".into()
162    }
163
164    fn strip_prefix(&mut self, base: &Path) {
165        self.input.strip_prefix(base);
166    }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
170#[serde(rename_all = "camelCase")]
171pub struct CliSettings {
172    #[serde(default, skip_serializing_if = "Vec::is_empty")]
173    pub extra_args: Vec<String>,
174    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
175    pub allow_paths: BTreeSet<PathBuf>,
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub base_path: Option<PathBuf>,
178    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
179    pub include_paths: BTreeSet<PathBuf>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
183pub struct SolcSettings {
184    /// JSON settings expected by Solc
185    #[serde(flatten)]
186    pub settings: Settings,
187    /// Additional CLI args configuration
188    #[serde(flatten)]
189    pub cli_settings: CliSettings,
190}
191
192impl Deref for SolcSettings {
193    type Target = Settings;
194
195    fn deref(&self) -> &Self::Target {
196        &self.settings
197    }
198}
199
200impl DerefMut for SolcSettings {
201    fn deref_mut(&mut self) -> &mut Self::Target {
202        &mut self.settings
203    }
204}
205
206/// Abstraction over min/max restrictions on some value.
207#[derive(Debug, Clone, Copy, Eq, Default, PartialEq)]
208pub struct Restriction<V> {
209    pub min: Option<V>,
210    pub max: Option<V>,
211}
212
213impl<V: Ord + Copy> Restriction<V> {
214    /// Returns true if the given value satisfies the restrictions
215    ///
216    /// If given None, only returns true if no restrictions are set
217    pub fn satisfies(&self, value: Option<V>) -> bool {
218        self.min.is_none_or(|min| value.is_some_and(|v| v >= min))
219            && self.max.is_none_or(|max| value.is_some_and(|v| v <= max))
220    }
221
222    /// Combines two restrictions into a new one
223    pub fn merge(self, other: Self) -> Option<Self> {
224        let Self { mut min, mut max } = self;
225        let Self { min: other_min, max: other_max } = other;
226
227        min = min.map_or(other_min, |this_min| {
228            Some(other_min.map_or(this_min, |other_min| this_min.max(other_min)))
229        });
230        max = max.map_or(other_max, |this_max| {
231            Some(other_max.map_or(this_max, |other_max| this_max.min(other_max)))
232        });
233
234        if let (Some(min), Some(max)) = (min, max) {
235            if min > max {
236                return None;
237            }
238        }
239
240        Some(Self { min, max })
241    }
242
243    pub fn apply(&self, value: Option<V>) -> Option<V> {
244        match (value, self.min, self.max) {
245            (None, Some(min), _) => Some(min),
246            (None, None, Some(max)) => Some(max),
247            (Some(cur), Some(min), _) if cur < min => Some(min),
248            (Some(cur), _, Some(max)) if cur > max => Some(max),
249            _ => value,
250        }
251    }
252}
253
254/// Restrictions on settings for the solc compiler.
255#[derive(Debug, Clone, Copy, Default)]
256pub struct SolcRestrictions {
257    pub evm_version: Restriction<EvmVersion>,
258    pub via_ir: Option<bool>,
259    pub optimizer_runs: Restriction<usize>,
260    pub bytecode_hash: Option<BytecodeHash>,
261}
262
263impl CompilerSettingsRestrictions for SolcRestrictions {
264    fn merge(self, other: Self) -> Option<Self> {
265        if let (Some(via_ir), Some(other_via_ir)) = (self.via_ir, other.via_ir) {
266            if via_ir != other_via_ir {
267                return None;
268            }
269        }
270
271        if let (Some(bytecode_hash), Some(other_bytecode_hash)) =
272            (self.bytecode_hash, other.bytecode_hash)
273        {
274            if bytecode_hash != other_bytecode_hash {
275                return None;
276            }
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_mut().enter_mut(|compiler| {
403            let mut pcx = compiler.parse();
404            let files = sources
405                .par_iter()
406                .map(|(path, source)| {
407                    pcx.sess
408                        .source_map()
409                        .new_source_file(path.clone(), source.content.as_str())
410                        .map_err(|e| SolcError::Io(SolcIoError::new(e, path)))
411                })
412                .collect::<Result<Vec<_>>>()?;
413            pcx.add_files(files);
414            pcx.parse();
415
416            let parsed = sources.par_iter().map(|(path, source)| {
417                let sf = compiler.sess().source_map().get_file(path).unwrap();
418                let (_, s) = compiler.gcx().sources.get_file(&sf).unwrap();
419                let node = Node::new(
420                    path.clone(),
421                    source.clone(),
422                    SolData::parse_from(compiler.gcx().sess, s),
423                );
424                (path.clone(), node)
425            });
426            let mut parsed = parsed.collect::<Vec<_>>();
427
428            // Set error on the first successful source, if any. This doesn't really have to be
429            // exact, as long as at least one source has an error set it should be enough.
430            if let Some(Err(diag)) = compiler.gcx().sess.emitted_errors() {
431                if let Some(idx) = parsed
432                    .iter()
433                    .position(|(_, node)| node.data.parse_result.is_ok())
434                    .or_else(|| parsed.first().map(|_| 0))
435                {
436                    let (_, node) = &mut parsed[idx];
437                    node.data.parse_result = Err(diag.to_string());
438                }
439            }
440
441            for (path, node) in &parsed {
442                if let Err(e) = &node.data.parse_result {
443                    debug!("failed parsing {}: {e}", path.display());
444                }
445            }
446
447            Ok(parsed)
448        })
449    }
450}
451
452impl ParsedSource for SolData {
453    type Language = SolcLanguage;
454
455    fn parse(content: &str, file: &std::path::Path) -> Result<Self> {
456        Ok(Self::parse(content, file))
457    }
458
459    fn version_req(&self) -> Option<&semver::VersionReq> {
460        self.version_req.as_ref()
461    }
462
463    fn contract_names(&self) -> &[String] {
464        &self.contract_names
465    }
466
467    fn language(&self) -> Self::Language {
468        if self.is_yul {
469            SolcLanguage::Yul
470        } else {
471            SolcLanguage::Solidity
472        }
473    }
474
475    fn resolve_imports<C>(
476        &self,
477        _paths: &crate::ProjectPathsConfig<C>,
478        _include_paths: &mut BTreeSet<PathBuf>,
479    ) -> Result<Vec<PathBuf>> {
480        Ok(self.imports.iter().map(|i| i.data().path().to_path_buf()).collect())
481    }
482
483    fn compilation_dependencies<'a>(
484        &self,
485        imported_nodes: impl Iterator<Item = (&'a Path, &'a Self)>,
486    ) -> impl Iterator<Item = &'a Path>
487    where
488        Self: 'a,
489    {
490        imported_nodes.filter_map(|(path, node)| (!node.libraries.is_empty()).then_some(path))
491    }
492}
493
494impl CompilationError for Error {
495    fn is_warning(&self) -> bool {
496        self.severity.is_warning()
497    }
498    fn is_error(&self) -> bool {
499        self.severity.is_error()
500    }
501
502    fn source_location(&self) -> Option<SourceLocation> {
503        self.source_location.clone()
504    }
505
506    fn severity(&self) -> Severity {
507        self.severity
508    }
509
510    fn error_code(&self) -> Option<u64> {
511        self.error_code
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use foundry_compilers_artifacts::{CompilerOutput, SolcLanguage};
518    use semver::Version;
519
520    use crate::{
521        buildinfo::RawBuildInfo,
522        compilers::{
523            solc::{SolcCompiler, SolcVersionedInput},
524            CompilerInput,
525        },
526        AggregatedCompilerOutput,
527    };
528
529    #[test]
530    fn can_parse_declaration_error() {
531        let s = r#"{
532  "errors": [
533    {
534      "component": "general",
535      "errorCode": "7576",
536      "formattedMessage": "DeclarationError: Undeclared identifier. Did you mean \"revert\"?\n  --> /Users/src/utils/UpgradeProxy.sol:35:17:\n   |\n35 |                 refert(\"Transparent ERC1967 proxies do not have upgradeable implementations\");\n   |                 ^^^^^^\n\n",
537      "message": "Undeclared identifier. Did you mean \"revert\"?",
538      "severity": "error",
539      "sourceLocation": {
540        "end": 1623,
541        "file": "/Users/src/utils/UpgradeProxy.sol",
542        "start": 1617
543      },
544      "type": "DeclarationError"
545    }
546  ],
547  "sources": { }
548}"#;
549
550        let out: CompilerOutput = serde_json::from_str(s).unwrap();
551        assert_eq!(out.errors.len(), 1);
552
553        let out_converted = crate::compilers::CompilerOutput {
554            errors: out.errors,
555            contracts: Default::default(),
556            sources: Default::default(),
557            metadata: Default::default(),
558        };
559
560        let v = Version::new(0, 8, 12);
561        let input = SolcVersionedInput::build(
562            Default::default(),
563            Default::default(),
564            SolcLanguage::Solidity,
565            v.clone(),
566        );
567        let build_info = RawBuildInfo::new(&input, &out_converted, true).unwrap();
568        let mut aggregated = AggregatedCompilerOutput::<SolcCompiler>::default();
569        aggregated.extend(v, build_info, "default", out_converted);
570        assert!(!aggregated.is_unchanged());
571    }
572}