foundry_compilers/compilers/solc/
mod.rs

1use super::{
2    restrictions::CompilerSettingsRestrictions, CompilationError, Compiler, CompilerInput,
3    CompilerOutput, CompilerSettings, CompilerVersion, Language, ParsedSource,
4};
5use crate::resolver::parse::SolData;
6pub use foundry_compilers_artifacts::SolcLanguage;
7use foundry_compilers_artifacts::{
8    error::SourceLocation,
9    output_selection::OutputSelection,
10    remappings::Remapping,
11    sources::{Source, Sources},
12    BytecodeHash, Contract, Error, EvmVersion, Settings, Severity, SolcInput,
13};
14use foundry_compilers_core::error::Result;
15use semver::Version;
16use serde::{Deserialize, Serialize};
17use std::{
18    borrow::Cow,
19    collections::{BTreeMap, BTreeSet},
20    ops::{Deref, DerefMut},
21    path::{Path, PathBuf},
22};
23mod compiler;
24pub use compiler::{Solc, SOLC_EXTENSIONS};
25
26#[derive(Clone, Debug)]
27#[cfg_attr(feature = "svm-solc", derive(Default))]
28pub enum SolcCompiler {
29    #[default]
30    #[cfg(feature = "svm-solc")]
31    AutoDetect,
32
33    Specific(Solc),
34}
35
36impl Language for SolcLanguage {
37    const FILE_EXTENSIONS: &'static [&'static str] = SOLC_EXTENSIONS;
38}
39
40impl Compiler for SolcCompiler {
41    type Input = SolcVersionedInput;
42    type CompilationError = Error;
43    type ParsedSource = SolData;
44    type Settings = SolcSettings;
45    type Language = SolcLanguage;
46    type CompilerContract = Contract;
47
48    fn compile(
49        &self,
50        input: &Self::Input,
51    ) -> Result<CompilerOutput<Self::CompilationError, Self::CompilerContract>> {
52        let mut solc = match self {
53            Self::Specific(solc) => solc.clone(),
54
55            #[cfg(feature = "svm-solc")]
56            Self::AutoDetect => Solc::find_or_install(&input.version)?,
57        };
58        solc.base_path.clone_from(&input.cli_settings.base_path);
59        solc.allow_paths.clone_from(&input.cli_settings.allow_paths);
60        solc.include_paths.clone_from(&input.cli_settings.include_paths);
61        solc.extra_args.extend_from_slice(&input.cli_settings.extra_args);
62
63        let solc_output = solc.compile(&input.input)?;
64
65        let output = CompilerOutput {
66            errors: solc_output.errors,
67            contracts: solc_output.contracts,
68            sources: solc_output.sources,
69            metadata: BTreeMap::new(),
70        };
71
72        Ok(output)
73    }
74
75    fn available_versions(&self, _language: &Self::Language) -> Vec<CompilerVersion> {
76        match self {
77            Self::Specific(solc) => vec![CompilerVersion::Installed(Version::new(
78                solc.version.major,
79                solc.version.minor,
80                solc.version.patch,
81            ))],
82
83            #[cfg(feature = "svm-solc")]
84            Self::AutoDetect => {
85                let mut all_versions = Solc::installed_versions()
86                    .into_iter()
87                    .map(CompilerVersion::Installed)
88                    .collect::<Vec<_>>();
89                let mut uniques = all_versions
90                    .iter()
91                    .map(|v| {
92                        let v = v.as_ref();
93                        (v.major, v.minor, v.patch)
94                    })
95                    .collect::<std::collections::HashSet<_>>();
96                all_versions.extend(
97                    Solc::released_versions()
98                        .into_iter()
99                        .filter(|v| uniques.insert((v.major, v.minor, v.patch)))
100                        .map(CompilerVersion::Remote),
101                );
102                all_versions.sort_unstable();
103                all_versions
104            }
105        }
106    }
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct SolcVersionedInput {
112    pub version: Version,
113    #[serde(flatten)]
114    pub input: SolcInput,
115    #[serde(flatten)]
116    pub cli_settings: CliSettings,
117}
118
119impl CompilerInput for SolcVersionedInput {
120    type Settings = SolcSettings;
121    type Language = SolcLanguage;
122
123    /// Creates a new [CompilerInput]s with default settings and the given sources
124    ///
125    /// A [CompilerInput] expects a language setting, supported by solc are solidity or yul.
126    /// In case the `sources` is a mix of solidity and yul files, 2 CompilerInputs are returned
127    fn build(
128        sources: Sources,
129        settings: Self::Settings,
130        language: Self::Language,
131        version: Version,
132    ) -> Self {
133        let SolcSettings { settings, cli_settings } = settings;
134        let input = SolcInput::new(language, sources, settings).sanitized(&version);
135
136        Self { version, input, cli_settings }
137    }
138
139    fn language(&self) -> Self::Language {
140        self.input.language
141    }
142
143    fn version(&self) -> &Version {
144        &self.version
145    }
146
147    fn sources(&self) -> impl Iterator<Item = (&Path, &Source)> {
148        self.input.sources.iter().map(|(path, source)| (path.as_path(), source))
149    }
150
151    fn compiler_name(&self) -> Cow<'static, str> {
152        "Solc".into()
153    }
154
155    fn strip_prefix(&mut self, base: &Path) {
156        self.input.strip_prefix(base);
157    }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
161#[serde(rename_all = "camelCase")]
162pub struct CliSettings {
163    #[serde(default, skip_serializing_if = "Vec::is_empty")]
164    pub extra_args: Vec<String>,
165    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
166    pub allow_paths: BTreeSet<PathBuf>,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub base_path: Option<PathBuf>,
169    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
170    pub include_paths: BTreeSet<PathBuf>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
174pub struct SolcSettings {
175    /// JSON settings expected by Solc
176    #[serde(flatten)]
177    pub settings: Settings,
178    /// Additional CLI args configuration
179    #[serde(flatten)]
180    pub cli_settings: CliSettings,
181}
182
183impl Deref for SolcSettings {
184    type Target = Settings;
185
186    fn deref(&self) -> &Self::Target {
187        &self.settings
188    }
189}
190
191impl DerefMut for SolcSettings {
192    fn deref_mut(&mut self) -> &mut Self::Target {
193        &mut self.settings
194    }
195}
196
197/// Abstraction over min/max restrictions on some value.
198#[derive(Debug, Clone, Copy, Eq, Default, PartialEq)]
199pub struct Restriction<V> {
200    pub min: Option<V>,
201    pub max: Option<V>,
202}
203
204impl<V: Ord + Copy> Restriction<V> {
205    /// Returns true if the given value satisfies the restrictions
206    ///
207    /// If given None, only returns true if no restrictions are set
208    pub fn satisfies(&self, value: Option<V>) -> bool {
209        self.min.is_none_or(|min| value.is_some_and(|v| v >= min))
210            && self.max.is_none_or(|max| value.is_some_and(|v| v <= max))
211    }
212
213    /// Combines two restrictions into a new one
214    pub fn merge(self, other: Self) -> Option<Self> {
215        let Self { mut min, mut max } = self;
216        let Self { min: other_min, max: other_max } = other;
217
218        min = min.map_or(other_min, |this_min| {
219            Some(other_min.map_or(this_min, |other_min| this_min.max(other_min)))
220        });
221        max = max.map_or(other_max, |this_max| {
222            Some(other_max.map_or(this_max, |other_max| this_max.min(other_max)))
223        });
224
225        if let (Some(min), Some(max)) = (min, max) {
226            if min > max {
227                return None;
228            }
229        }
230
231        Some(Self { min, max })
232    }
233
234    pub fn apply(&self, value: Option<V>) -> Option<V> {
235        match (value, self.min, self.max) {
236            (None, Some(min), _) => Some(min),
237            (None, None, Some(max)) => Some(max),
238            (Some(cur), Some(min), _) if cur < min => Some(min),
239            (Some(cur), _, Some(max)) if cur > max => Some(max),
240            _ => value,
241        }
242    }
243}
244
245/// Restrictions on settings for the solc compiler.
246#[derive(Debug, Clone, Copy, Default)]
247pub struct SolcRestrictions {
248    pub evm_version: Restriction<EvmVersion>,
249    pub via_ir: Option<bool>,
250    pub optimizer_runs: Restriction<usize>,
251    pub bytecode_hash: Option<BytecodeHash>,
252}
253
254impl CompilerSettingsRestrictions for SolcRestrictions {
255    fn merge(self, other: Self) -> Option<Self> {
256        if let (Some(via_ir), Some(other_via_ir)) = (self.via_ir, other.via_ir) {
257            if via_ir != other_via_ir {
258                return None;
259            }
260        }
261
262        if let (Some(bytecode_hash), Some(other_bytecode_hash)) =
263            (self.bytecode_hash, other.bytecode_hash)
264        {
265            if bytecode_hash != other_bytecode_hash {
266                return None;
267            }
268        }
269
270        Some(Self {
271            evm_version: self.evm_version.merge(other.evm_version)?,
272            via_ir: self.via_ir.or(other.via_ir),
273            optimizer_runs: self.optimizer_runs.merge(other.optimizer_runs)?,
274            bytecode_hash: self.bytecode_hash.or(other.bytecode_hash),
275        })
276    }
277}
278
279impl CompilerSettings for SolcSettings {
280    type Restrictions = SolcRestrictions;
281
282    fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) {
283        f(&mut self.settings.output_selection)
284    }
285
286    fn can_use_cached(&self, other: &Self) -> bool {
287        let Self {
288            settings:
289                Settings {
290                    stop_after,
291                    remappings,
292                    optimizer,
293                    model_checker,
294                    metadata,
295                    output_selection,
296                    evm_version,
297                    via_ir,
298                    debug,
299                    libraries,
300                },
301            ..
302        } = self;
303
304        *stop_after == other.settings.stop_after
305            && *remappings == other.settings.remappings
306            && *optimizer == other.settings.optimizer
307            && *model_checker == other.settings.model_checker
308            && *metadata == other.settings.metadata
309            && *evm_version == other.settings.evm_version
310            && *via_ir == other.settings.via_ir
311            && *debug == other.settings.debug
312            && *libraries == other.settings.libraries
313            && output_selection.is_subset_of(&other.settings.output_selection)
314    }
315
316    fn with_remappings(mut self, remappings: &[Remapping]) -> Self {
317        self.settings.remappings = remappings.to_vec();
318
319        self
320    }
321
322    fn with_allow_paths(mut self, allowed_paths: &BTreeSet<PathBuf>) -> Self {
323        self.cli_settings.allow_paths.clone_from(allowed_paths);
324        self
325    }
326
327    fn with_base_path(mut self, base_path: &Path) -> Self {
328        self.cli_settings.base_path = Some(base_path.to_path_buf());
329        self
330    }
331
332    fn with_include_paths(mut self, include_paths: &BTreeSet<PathBuf>) -> Self {
333        self.cli_settings.include_paths.clone_from(include_paths);
334        self
335    }
336
337    fn satisfies_restrictions(&self, restrictions: &Self::Restrictions) -> bool {
338        let mut satisfies = true;
339
340        let SolcRestrictions { evm_version, via_ir, optimizer_runs, bytecode_hash } = restrictions;
341
342        satisfies &= evm_version.satisfies(self.evm_version);
343        satisfies &= via_ir.is_none_or(|via_ir| via_ir == self.via_ir.unwrap_or_default());
344        satisfies &= bytecode_hash.is_none_or(|bytecode_hash| {
345            self.metadata.as_ref().and_then(|m| m.bytecode_hash) == Some(bytecode_hash)
346        });
347        satisfies &= optimizer_runs.satisfies(self.optimizer.runs);
348
349        // Ensure that we either don't have min optimizer runs set or that the optimizer is enabled
350        satisfies &= optimizer_runs
351            .min
352            .is_none_or(|min| min == 0 || self.optimizer.enabled.unwrap_or_default());
353
354        satisfies
355    }
356}
357
358impl ParsedSource for SolData {
359    type Language = SolcLanguage;
360
361    fn parse(content: &str, file: &std::path::Path) -> Result<Self> {
362        Ok(Self::parse(content, file))
363    }
364
365    fn version_req(&self) -> Option<&semver::VersionReq> {
366        self.version_req.as_ref()
367    }
368
369    fn contract_names(&self) -> &[String] {
370        &self.contract_names
371    }
372
373    fn language(&self) -> Self::Language {
374        if self.is_yul {
375            SolcLanguage::Yul
376        } else {
377            SolcLanguage::Solidity
378        }
379    }
380
381    fn resolve_imports<C>(
382        &self,
383        _paths: &crate::ProjectPathsConfig<C>,
384        _include_paths: &mut BTreeSet<PathBuf>,
385    ) -> Result<Vec<PathBuf>> {
386        Ok(self.imports.iter().map(|i| i.data().path().to_path_buf()).collect())
387    }
388
389    fn compilation_dependencies<'a>(
390        &self,
391        imported_nodes: impl Iterator<Item = (&'a Path, &'a Self)>,
392    ) -> impl Iterator<Item = &'a Path>
393    where
394        Self: 'a,
395    {
396        imported_nodes.filter_map(|(path, node)| (!node.libraries.is_empty()).then_some(path))
397    }
398}
399
400impl CompilationError for Error {
401    fn is_warning(&self) -> bool {
402        self.severity.is_warning()
403    }
404    fn is_error(&self) -> bool {
405        self.severity.is_error()
406    }
407
408    fn source_location(&self) -> Option<SourceLocation> {
409        self.source_location.clone()
410    }
411
412    fn severity(&self) -> Severity {
413        self.severity
414    }
415
416    fn error_code(&self) -> Option<u64> {
417        self.error_code
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use foundry_compilers_artifacts::{CompilerOutput, SolcLanguage};
424    use semver::Version;
425
426    use crate::{
427        buildinfo::RawBuildInfo,
428        compilers::{
429            solc::{SolcCompiler, SolcVersionedInput},
430            CompilerInput,
431        },
432        AggregatedCompilerOutput,
433    };
434
435    #[test]
436    fn can_parse_declaration_error() {
437        let s = r#"{
438  "errors": [
439    {
440      "component": "general",
441      "errorCode": "7576",
442      "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",
443      "message": "Undeclared identifier. Did you mean \"revert\"?",
444      "severity": "error",
445      "sourceLocation": {
446        "end": 1623,
447        "file": "/Users/src/utils/UpgradeProxy.sol",
448        "start": 1617
449      },
450      "type": "DeclarationError"
451    }
452  ],
453  "sources": { }
454}"#;
455
456        let out: CompilerOutput = serde_json::from_str(s).unwrap();
457        assert_eq!(out.errors.len(), 1);
458
459        let out_converted = crate::compilers::CompilerOutput {
460            errors: out.errors,
461            contracts: Default::default(),
462            sources: Default::default(),
463            metadata: Default::default(),
464        };
465
466        let v = Version::new(0, 8, 12);
467        let input = SolcVersionedInput::build(
468            Default::default(),
469            Default::default(),
470            SolcLanguage::Solidity,
471            v.clone(),
472        );
473        let build_info = RawBuildInfo::new(&input, &out_converted, true).unwrap();
474        let mut aggregated = AggregatedCompilerOutput::<SolcCompiler>::default();
475        aggregated.extend(v, build_info, "default", out_converted);
476        assert!(!aggregated.is_unchanged());
477    }
478}