foundry_config/
lib.rs

1//! # foundry-config
2//!
3//! Foundry configuration.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use crate::cache::StorageCachingConfig;
12use alloy_primitives::{Address, B256, FixedBytes, U256, address, map::AddressHashMap};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15    Error, Figment, Metadata, Profile, Provider,
16    providers::{Env, Format, Serialized, Toml},
17    value::{Dict, Map, Value},
18};
19use filter::GlobMatcher;
20use foundry_compilers::{
21    ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
22    RestrictionsWithVersion, VyperLanguage,
23    artifacts::{
24        BytecodeHash, DebuggingSettings, EvmVersion, Libraries, ModelCheckerSettings,
25        ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings, Settings, SettingsMetadata,
26        Severity,
27        output_selection::{ContractOutputSelection, OutputSelection},
28        remappings::{RelativeRemapping, Remapping},
29        serde_helpers,
30    },
31    cache::SOLIDITY_FILES_CACHE_FILENAME,
32    compilers::{
33        Compiler,
34        multi::{MultiCompiler, MultiCompilerSettings},
35        solc::{Solc, SolcCompiler},
36        vyper::{Vyper, VyperSettings},
37    },
38    error::SolcError,
39    multi::{MultiCompilerParser, MultiCompilerRestrictions},
40    solc::{CliSettings, SolcLanguage, SolcSettings},
41};
42use regex::Regex;
43use revm::primitives::hardfork::SpecId;
44use semver::Version;
45use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
46use std::{
47    borrow::Cow,
48    collections::BTreeMap,
49    fs,
50    path::{Path, PathBuf},
51    str::FromStr,
52};
53
54mod macros;
55
56pub mod utils;
57pub use utils::*;
58
59mod endpoints;
60pub use endpoints::{
61    ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
62};
63
64mod etherscan;
65pub use etherscan::EtherscanConfigError;
66use etherscan::{EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig};
67
68pub mod resolve;
69pub use resolve::UnresolvedEnvVarError;
70
71pub mod cache;
72use cache::{Cache, ChainCache};
73
74pub mod fmt;
75pub use fmt::FormatterConfig;
76
77pub mod lint;
78pub use lint::{LinterConfig, Severity as LintSeverity};
79
80pub mod fs_permissions;
81pub use fs_permissions::FsPermissions;
82use fs_permissions::PathPermission;
83
84pub mod error;
85use error::ExtractConfigError;
86pub use error::SolidityErrorCode;
87
88pub mod doc;
89pub use doc::DocConfig;
90
91pub mod filter;
92pub use filter::SkipBuildFilters;
93
94mod warning;
95pub use warning::*;
96
97pub mod fix;
98
99// reexport so cli types can implement `figment::Provider` to easily merge compiler arguments
100pub use alloy_chains::{Chain, NamedChain};
101pub use figment;
102
103pub mod providers;
104pub use providers::Remappings;
105use providers::*;
106
107mod fuzz;
108pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig};
109
110mod invariant;
111pub use invariant::InvariantConfig;
112
113mod inline;
114pub use inline::{InlineConfig, InlineConfigError, NatSpec};
115
116pub mod soldeer;
117use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
118
119mod vyper;
120pub use vyper::VyperConfig;
121
122mod bind_json;
123use bind_json::BindJsonConfig;
124
125mod compilation;
126pub use compilation::{CompilationRestrictions, SettingsOverrides};
127
128pub mod extend;
129use extend::Extends;
130
131use foundry_evm_networks::NetworkConfigs;
132pub use semver;
133
134/// Foundry configuration
135///
136/// # Defaults
137///
138/// All configuration values have a default, documented in the [fields](#fields)
139/// section below. [`Config::default()`] returns the default values for
140/// the default profile while [`Config::with_root()`] returns the values based on the given
141/// directory. [`Config::load()`] starts with the default profile and merges various providers into
142/// the config, same for [`Config::load_with_root()`], but there the default values are determined
143/// by [`Config::with_root()`]
144///
145/// # Provider Details
146///
147/// `Config` is a Figment [`Provider`] with the following characteristics:
148///
149///   * **Profile**
150///
151///     The profile is set to the value of the `profile` field.
152///
153///   * **Metadata**
154///
155///     This provider is named `Foundry Config`. It does not specify a
156///     [`Source`](figment::Source) and uses default interpolation.
157///
158///   * **Data**
159///
160///     The data emitted by this provider are the keys and values corresponding
161///     to the fields and values of the structure. The dictionary is emitted to
162///     the "default" meta-profile.
163///
164/// Note that these behaviors differ from those of [`Config::figment()`].
165#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
166pub struct Config {
167    /// The selected profile. **(default: _default_ `default`)**
168    ///
169    /// **Note:** This field is never serialized nor deserialized. When a
170    /// `Config` is merged into a `Figment` as a `Provider`, this profile is
171    /// selected on the `Figment`. When a `Config` is extracted, this field is
172    /// set to the extracting Figment's selected `Profile`.
173    #[serde(skip)]
174    pub profile: Profile,
175    /// The list of all profiles defined in the config.
176    ///
177    /// See `profile`.
178    #[serde(skip)]
179    pub profiles: Vec<Profile>,
180
181    /// The root path where the config detection started from, [`Config::with_root`].
182    // We're skipping serialization here, so it won't be included in the [`Config::to_string()`]
183    // representation, but will be deserialized from the `Figment` so that forge commands can
184    // override it.
185    #[serde(default = "root_default", skip_serializing)]
186    pub root: PathBuf,
187
188    /// Configuration for extending from another foundry.toml (base) file.
189    ///
190    /// Can be either a string path or an object with path and strategy.
191    /// Base files cannot extend (inherit) other files.
192    #[serde(default, skip_serializing)]
193    pub extends: Option<Extends>,
194
195    /// path of the source contracts dir, like `src` or `contracts`
196    pub src: PathBuf,
197    /// path of the test dir
198    pub test: PathBuf,
199    /// path of the script dir
200    pub script: PathBuf,
201    /// path to where artifacts shut be written to
202    pub out: PathBuf,
203    /// all library folders to include, `lib`, `node_modules`
204    pub libs: Vec<PathBuf>,
205    /// `Remappings` to use for this repo
206    pub remappings: Vec<RelativeRemapping>,
207    /// Whether to autodetect remappings by scanning the `libs` folders recursively
208    pub auto_detect_remappings: bool,
209    /// library addresses to link
210    pub libraries: Vec<String>,
211    /// whether to enable cache
212    pub cache: bool,
213    /// whether to dynamically link tests
214    pub dynamic_test_linking: bool,
215    /// where the cache is stored if enabled
216    pub cache_path: PathBuf,
217    /// where the gas snapshots are stored
218    pub snapshots: PathBuf,
219    /// whether to check for differences against previously stored gas snapshots
220    pub gas_snapshot_check: bool,
221    /// whether to emit gas snapshots to disk
222    pub gas_snapshot_emit: bool,
223    /// where the broadcast logs are stored
224    pub broadcast: PathBuf,
225    /// additional solc allow paths for `--allow-paths`
226    pub allow_paths: Vec<PathBuf>,
227    /// additional solc include paths for `--include-path`
228    pub include_paths: Vec<PathBuf>,
229    /// glob patterns to skip
230    pub skip: Vec<GlobMatcher>,
231    /// whether to force a `project.clean()`
232    pub force: bool,
233    /// evm version to use
234    #[serde(with = "from_str_lowercase")]
235    pub evm_version: EvmVersion,
236    /// list of contracts to report gas of
237    pub gas_reports: Vec<String>,
238    /// list of contracts to ignore for gas reports
239    pub gas_reports_ignore: Vec<String>,
240    /// Whether to include gas reports for tests.
241    pub gas_reports_include_tests: bool,
242    /// The Solc instance to use if any.
243    ///
244    /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides
245    /// auto-detection.
246    ///
247    /// **Note** for backwards compatibility reasons this also accepts solc_version from the toml
248    /// file, see `BackwardsCompatTomlProvider`.
249    ///
250    /// Avoid using this field directly; call the related `solc` methods instead.
251    #[doc(hidden)]
252    pub solc: Option<SolcReq>,
253    /// Whether to autodetect the solc compiler version to use.
254    pub auto_detect_solc: bool,
255    /// Offline mode, if set, network access (downloading solc) is disallowed.
256    ///
257    /// Relationship with `auto_detect_solc`:
258    ///    - if `auto_detect_solc = true` and `offline = true`, the required solc version(s) will
259    ///      be auto detected but if the solc version is not installed, it will _not_ try to
260    ///      install it
261    pub offline: bool,
262    /// Whether to activate optimizer
263    pub optimizer: Option<bool>,
264    /// The number of runs specifies roughly how often each opcode of the deployed code will be
265    /// executed across the life-time of the contract. This means it is a trade-off parameter
266    /// between code size (deploy cost) and code execution cost (cost after deployment).
267    /// An `optimizer_runs` parameter of `1` will produce short but expensive code. In contrast, a
268    /// larger `optimizer_runs` parameter will produce longer but more gas efficient code. The
269    /// maximum value of the parameter is `2**32-1`.
270    ///
271    /// A common misconception is that this parameter specifies the number of iterations of the
272    /// optimizer. This is not true: The optimizer will always run as many times as it can
273    /// still improve the code.
274    pub optimizer_runs: Option<usize>,
275    /// Switch optimizer components on or off in detail.
276    /// The "enabled" switch above provides two defaults which can be
277    /// tweaked here. If "details" is given, "enabled" can be omitted.
278    pub optimizer_details: Option<OptimizerDetails>,
279    /// Model checker settings.
280    pub model_checker: Option<ModelCheckerSettings>,
281    /// verbosity to use
282    pub verbosity: u8,
283    /// url of the rpc server that should be used for any rpc calls
284    pub eth_rpc_url: Option<String>,
285    /// Whether to accept invalid certificates for the rpc server.
286    pub eth_rpc_accept_invalid_certs: bool,
287    /// JWT secret that should be used for any rpc calls
288    pub eth_rpc_jwt: Option<String>,
289    /// Timeout that should be used for any rpc calls
290    pub eth_rpc_timeout: Option<u64>,
291    /// Headers that should be used for any rpc calls
292    ///
293    /// # Example
294    ///
295    /// rpc_headers = ["x-custom-header:value", "x-another-header:another-value"]
296    ///
297    /// You can also the ETH_RPC_HEADERS env variable like so:
298    /// `ETH_RPC_HEADERS="x-custom-header:value x-another-header:another-value"`
299    pub eth_rpc_headers: Option<Vec<String>>,
300    /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table
301    pub etherscan_api_key: Option<String>,
302    /// Multiple etherscan api configs and their aliases
303    #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
304    pub etherscan: EtherscanConfigs,
305    /// list of solidity error codes to always silence in the compiler output
306    pub ignored_error_codes: Vec<SolidityErrorCode>,
307    /// list of file paths to ignore
308    #[serde(rename = "ignored_warnings_from")]
309    pub ignored_file_paths: Vec<PathBuf>,
310    /// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
311    pub deny: DenyLevel,
312    /// DEPRECATED: use `deny` instead.
313    #[serde(default, skip_serializing)]
314    pub deny_warnings: bool,
315    /// Only run test functions matching the specified regex pattern.
316    #[serde(rename = "match_test")]
317    pub test_pattern: Option<RegexWrapper>,
318    /// Only run test functions that do not match the specified regex pattern.
319    #[serde(rename = "no_match_test")]
320    pub test_pattern_inverse: Option<RegexWrapper>,
321    /// Only run tests in contracts matching the specified regex pattern.
322    #[serde(rename = "match_contract")]
323    pub contract_pattern: Option<RegexWrapper>,
324    /// Only run tests in contracts that do not match the specified regex pattern.
325    #[serde(rename = "no_match_contract")]
326    pub contract_pattern_inverse: Option<RegexWrapper>,
327    /// Only run tests in source files matching the specified glob pattern.
328    #[serde(rename = "match_path", with = "from_opt_glob")]
329    pub path_pattern: Option<globset::Glob>,
330    /// Only run tests in source files that do not match the specified glob pattern.
331    #[serde(rename = "no_match_path", with = "from_opt_glob")]
332    pub path_pattern_inverse: Option<globset::Glob>,
333    /// Only show coverage for files that do not match the specified regex pattern.
334    #[serde(rename = "no_match_coverage")]
335    pub coverage_pattern_inverse: Option<RegexWrapper>,
336    /// Path where last test run failures are recorded.
337    pub test_failures_file: PathBuf,
338    /// Max concurrent threads to use.
339    pub threads: Option<usize>,
340    /// Whether to show test execution progress.
341    pub show_progress: bool,
342    /// Configuration for fuzz testing
343    pub fuzz: FuzzConfig,
344    /// Configuration for invariant testing
345    pub invariant: InvariantConfig,
346    /// Whether to allow ffi cheatcodes in test
347    pub ffi: bool,
348    /// Whether to allow `expectRevert` for internal functions.
349    pub allow_internal_expect_revert: bool,
350    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
351    pub always_use_create_2_factory: bool,
352    /// Sets a timeout in seconds for vm.prompt cheatcodes
353    pub prompt_timeout: u64,
354    /// The address which will be executing all tests
355    pub sender: Address,
356    /// The tx.origin value during EVM execution
357    pub tx_origin: Address,
358    /// the initial balance of each deployed test contract
359    pub initial_balance: U256,
360    /// the block.number value during EVM execution
361    #[serde(
362        deserialize_with = "crate::deserialize_u64_to_u256",
363        serialize_with = "crate::serialize_u64_or_u256"
364    )]
365    pub block_number: U256,
366    /// pins the block number for the state fork
367    pub fork_block_number: Option<u64>,
368    /// The chain name or EIP-155 chain ID.
369    #[serde(rename = "chain_id", alias = "chain")]
370    pub chain: Option<Chain>,
371    /// Block gas limit.
372    pub gas_limit: GasLimit,
373    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
374    pub code_size_limit: Option<usize>,
375    /// `tx.gasprice` value during EVM execution.
376    ///
377    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
378    /// (if set by user) or the remote client's gas price.
379    pub gas_price: Option<u64>,
380    /// The base fee in a block.
381    pub block_base_fee_per_gas: u64,
382    /// The `block.coinbase` value during EVM execution.
383    pub block_coinbase: Address,
384    /// The `block.timestamp` value during EVM execution.
385    #[serde(
386        deserialize_with = "crate::deserialize_u64_to_u256",
387        serialize_with = "crate::serialize_u64_or_u256"
388    )]
389    pub block_timestamp: U256,
390    /// The `block.difficulty` value during EVM execution.
391    pub block_difficulty: u64,
392    /// Before merge the `block.max_hash`, after merge it is `block.prevrandao`.
393    pub block_prevrandao: B256,
394    /// The `block.gaslimit` value during EVM execution.
395    pub block_gas_limit: Option<GasLimit>,
396    /// The memory limit per EVM execution in bytes.
397    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
398    ///
399    /// The default is 128MiB.
400    pub memory_limit: u64,
401    /// Additional output selection for all contracts, such as "ir", "devdoc", "storageLayout",
402    /// etc.
403    ///
404    /// See the [Solc Compiler Api](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-api) for more information.
405    ///
406    /// The following values are always set because they're required by `forge`:
407    /// ```json
408    /// {
409    ///   "*": [
410    ///       "abi",
411    ///       "evm.bytecode",
412    ///       "evm.deployedBytecode",
413    ///       "evm.methodIdentifiers"
414    ///     ]
415    /// }
416    /// ```
417    #[serde(default)]
418    pub extra_output: Vec<ContractOutputSelection>,
419    /// If set, a separate JSON file will be emitted for every contract depending on the
420    /// selection, eg. `extra_output_files = ["metadata"]` will create a `metadata.json` for
421    /// each contract in the project.
422    ///
423    /// See [Contract Metadata](https://docs.soliditylang.org/en/latest/metadata.html) for more information.
424    ///
425    /// The difference between `extra_output = ["metadata"]` and
426    /// `extra_output_files = ["metadata"]` is that the former will include the
427    /// contract's metadata in the contract's json artifact, whereas the latter will emit the
428    /// output selection as separate files.
429    #[serde(default)]
430    pub extra_output_files: Vec<ContractOutputSelection>,
431    /// Whether to print the names of the compiled contracts.
432    pub names: bool,
433    /// Whether to print the sizes of the compiled contracts.
434    pub sizes: bool,
435    /// If set to true, changes compilation pipeline to go through the Yul intermediate
436    /// representation.
437    pub via_ir: bool,
438    /// Whether to include the AST as JSON in the compiler output.
439    pub ast: bool,
440    /// RPC storage caching settings determines what chains and endpoints to cache
441    pub rpc_storage_caching: StorageCachingConfig,
442    /// Disables storage caching entirely. This overrides any settings made in
443    /// `rpc_storage_caching`
444    pub no_storage_caching: bool,
445    /// Disables rate limiting entirely. This overrides any settings made in
446    /// `compute_units_per_second`
447    pub no_rpc_rate_limit: bool,
448    /// Multiple rpc endpoints and their aliases
449    #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
450    pub rpc_endpoints: RpcEndpoints,
451    /// Whether to store the referenced sources in the metadata as literal data.
452    pub use_literal_content: bool,
453    /// Whether to include the metadata hash.
454    ///
455    /// The metadata hash is machine dependent. By default, this is set to [BytecodeHash::None] to allow for deterministic code, See: <https://docs.soliditylang.org/en/latest/metadata.html>
456    #[serde(with = "from_str_lowercase")]
457    pub bytecode_hash: BytecodeHash,
458    /// Whether to append the metadata hash to the bytecode.
459    ///
460    /// If this is `false` and the `bytecode_hash` option above is not `None` solc will issue a
461    /// warning.
462    pub cbor_metadata: bool,
463    /// How to treat revert (and require) reason strings.
464    #[serde(with = "serde_helpers::display_from_str_opt")]
465    pub revert_strings: Option<RevertStrings>,
466    /// Whether to compile in sparse mode
467    ///
468    /// If this option is enabled, only the required contracts/files will be selected to be
469    /// included in solc's output selection, see also [`OutputSelection`].
470    pub sparse_mode: bool,
471    /// Generates additional build info json files for every new build, containing the
472    /// `CompilerInput` and `CompilerOutput`.
473    pub build_info: bool,
474    /// The path to the `build-info` directory that contains the build info json files.
475    pub build_info_path: Option<PathBuf>,
476    /// Configuration for `forge fmt`
477    pub fmt: FormatterConfig,
478    /// Configuration for `forge lint`
479    pub lint: LinterConfig,
480    /// Configuration for `forge doc`
481    pub doc: DocConfig,
482    /// Configuration for `forge bind-json`
483    pub bind_json: BindJsonConfig,
484    /// Configures the permissions of cheat codes that touch the file system.
485    ///
486    /// This includes what operations can be executed (read, write)
487    pub fs_permissions: FsPermissions,
488
489    /// Whether to enable call isolation.
490    ///
491    /// Useful for more correct gas accounting and EVM behavior in general.
492    pub isolate: bool,
493
494    /// Whether to disable the block gas limit checks.
495    pub disable_block_gas_limit: bool,
496
497    /// Whether to enable the tx gas limit checks as imposed by Osaka (EIP-7825).
498    pub enable_tx_gas_limit: bool,
499
500    /// Address labels
501    pub labels: AddressHashMap<String>,
502
503    /// Whether to enable safety checks for `vm.getCode` and `vm.getDeployedCode` invocations.
504    /// If disabled, it is possible to access artifacts which were not recompiled or cached.
505    pub unchecked_cheatcode_artifacts: bool,
506
507    /// CREATE2 salt to use for the library deployment in scripts.
508    pub create2_library_salt: B256,
509
510    /// The CREATE2 deployer address to use.
511    pub create2_deployer: Address,
512
513    /// Configuration for Vyper compiler
514    pub vyper: VyperConfig,
515
516    /// Soldeer dependencies
517    pub dependencies: Option<SoldeerDependencyConfig>,
518
519    /// Soldeer custom configs
520    pub soldeer: Option<SoldeerConfig>,
521
522    /// Whether failed assertions should revert.
523    ///
524    /// Note that this only applies to native (cheatcode) assertions, invoked on Vm contract.
525    pub assertions_revert: bool,
526
527    /// Whether `failed()` should be invoked to check if the test have failed.
528    pub legacy_assertions: bool,
529
530    /// Optional additional CLI arguments to pass to `solc` binary.
531    #[serde(default, skip_serializing_if = "Vec::is_empty")]
532    pub extra_args: Vec<String>,
533
534    /// Networks with enabled features.
535    #[serde(flatten)]
536    pub networks: NetworkConfigs,
537
538    /// Timeout for transactions in seconds.
539    pub transaction_timeout: u64,
540
541    /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information.
542    #[serde(rename = "__warnings", default, skip_serializing)]
543    pub warnings: Vec<Warning>,
544
545    /// Additional settings profiles to use when compiling.
546    #[serde(default)]
547    pub additional_compiler_profiles: Vec<SettingsOverrides>,
548
549    /// Restrictions on compilation of certain files.
550    #[serde(default)]
551    pub compilation_restrictions: Vec<CompilationRestrictions>,
552
553    /// Whether to enable script execution protection.
554    pub script_execution_protection: bool,
555
556    /// PRIVATE: This structure may grow, As such, constructing this structure should
557    /// _always_ be done using a public constructor or update syntax:
558    ///
559    /// ```ignore
560    /// use foundry_config::Config;
561    ///
562    /// let config = Config { src: "other".into(), ..Default::default() };
563    /// ```
564    #[doc(hidden)]
565    #[serde(skip)]
566    pub _non_exhaustive: (),
567}
568
569/// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
570#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
571#[serde(rename_all = "lowercase")]
572pub enum DenyLevel {
573    /// Always exit with zero code.
574    #[default]
575    Never,
576    /// Exit with a non-zero code if any warnings are found.
577    Warnings,
578    /// Exit with a non-zero code if any notes or warnings are found.
579    Notes,
580}
581
582// Custom deserialization to make `DenyLevel` parsing case-insensitive and backwards compatible with
583// booleans.
584impl<'de> Deserialize<'de> for DenyLevel {
585    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
586    where
587        D: Deserializer<'de>,
588    {
589        struct DenyLevelVisitor;
590
591        impl<'de> de::Visitor<'de> for DenyLevelVisitor {
592            type Value = DenyLevel;
593
594            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
595                formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
596            }
597
598            fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
599            where
600                E: de::Error,
601            {
602                Ok(DenyLevel::from(value))
603            }
604
605            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
606            where
607                E: de::Error,
608            {
609                DenyLevel::from_str(value).map_err(de::Error::custom)
610            }
611        }
612
613        deserializer.deserialize_any(DenyLevelVisitor)
614    }
615}
616
617impl FromStr for DenyLevel {
618    type Err = String;
619
620    fn from_str(s: &str) -> Result<Self, Self::Err> {
621        match s.to_lowercase().as_str() {
622            "warnings" | "warning" | "w" => Ok(Self::Warnings),
623            "notes" | "note" | "n" => Ok(Self::Notes),
624            "never" | "false" | "f" => Ok(Self::Never),
625            _ => Err(format!(
626                "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
627            )),
628        }
629    }
630}
631
632impl From<bool> for DenyLevel {
633    fn from(deny: bool) -> Self {
634        if deny { Self::Warnings } else { Self::Never }
635    }
636}
637
638impl DenyLevel {
639    /// Returns `true` if the deny level includes warnings.
640    pub fn warnings(&self) -> bool {
641        match self {
642            Self::Never => false,
643            Self::Warnings | Self::Notes => true,
644        }
645    }
646
647    /// Returns `true` if the deny level includes notes.
648    pub fn notes(&self) -> bool {
649        match self {
650            Self::Never | Self::Warnings => false,
651            Self::Notes => true,
652        }
653    }
654
655    /// Returns `true` if the deny level is set to never (only errors).
656    pub fn never(&self) -> bool {
657        match self {
658            Self::Never => true,
659            Self::Warnings | Self::Notes => false,
660        }
661    }
662}
663
664/// Mapping of fallback standalone sections. See [`FallbackProfileProvider`].
665pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
666
667/// Deprecated keys and their replacements.
668///
669/// See [Warning::DeprecatedKey]
670pub const DEPRECATIONS: &[(&str, &str)] =
671    &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
672
673impl Config {
674    /// The default profile: "default"
675    pub const DEFAULT_PROFILE: Profile = Profile::Default;
676
677    /// The hardhat profile: "hardhat"
678    pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
679
680    /// TOML section for profiles
681    pub const PROFILE_SECTION: &'static str = "profile";
682
683    /// External config sections, ignored from warnings.
684    pub const EXTERNAL_SECTION: &'static str = "external";
685
686    /// Standalone sections in the config which get integrated into the selected profile
687    pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
688        "rpc_endpoints",
689        "etherscan",
690        "fmt",
691        "lint",
692        "doc",
693        "fuzz",
694        "invariant",
695        "labels",
696        "dependencies",
697        "soldeer",
698        "vyper",
699        "bind_json",
700    ];
701
702    /// File name of config toml file
703    pub const FILE_NAME: &'static str = "foundry.toml";
704
705    /// The name of the directory foundry reserves for itself under the user's home directory: `~`
706    pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
707
708    /// Default address for tx.origin
709    ///
710    /// `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`
711    pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
712
713    /// Default salt for create2 library deployments
714    pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
715
716    /// Default create2 deployer
717    pub const DEFAULT_CREATE2_DEPLOYER: Address =
718        address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
719
720    /// Loads the `Config` from the current directory.
721    ///
722    /// See [`figment`](Self::figment) for more details.
723    pub fn load() -> Result<Self, ExtractConfigError> {
724        Self::from_provider(Self::figment())
725    }
726
727    /// Loads the `Config` with the given `providers` preset.
728    ///
729    /// See [`figment`](Self::figment) for more details.
730    pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
731        Self::from_provider(Self::default().to_figment(providers))
732    }
733
734    /// Loads the `Config` from the given root directory.
735    ///
736    /// See [`figment_with_root`](Self::figment_with_root) for more details.
737    #[track_caller]
738    pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
739        Self::from_provider(Self::figment_with_root(root.as_ref()))
740    }
741
742    /// Attempts to extract a `Config` from `provider`, returning the result.
743    ///
744    /// # Example
745    ///
746    /// ```rust
747    /// use figment::providers::{Env, Format, Toml};
748    /// use foundry_config::Config;
749    ///
750    /// // Use foundry's default `Figment`, but allow values from `other.toml`
751    /// // to supersede its values.
752    /// let figment = Config::figment().merge(Toml::file("other.toml").nested());
753    ///
754    /// let config = Config::from_provider(figment);
755    /// ```
756    #[doc(alias = "try_from")]
757    pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
758        trace!("load config with provider: {:?}", provider.metadata());
759        Self::from_figment(Figment::from(provider))
760    }
761
762    #[doc(hidden)]
763    #[deprecated(note = "use `Config::from_provider` instead")]
764    pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
765        Self::from_provider(provider)
766    }
767
768    fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
769        let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
770        config.profile = figment.profile().clone();
771
772        // The `"profile"` profile contains all the profiles as keys.
773        let mut add_profile = |profile: &Profile| {
774            if !config.profiles.contains(profile) {
775                config.profiles.push(profile.clone());
776            }
777        };
778        let figment = figment.select(Self::PROFILE_SECTION);
779        if let Ok(data) = figment.data()
780            && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
781        {
782            for profile in profiles.keys() {
783                add_profile(&Profile::new(profile));
784            }
785        }
786        add_profile(&Self::DEFAULT_PROFILE);
787        add_profile(&config.profile);
788
789        config.normalize_optimizer_settings();
790
791        Ok(config)
792    }
793
794    /// Returns the populated [Figment] using the requested [FigmentProviders] preset.
795    ///
796    /// This will merge various providers, such as env,toml,remappings into the figment if
797    /// requested.
798    pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
799        // Note that `Figment::from` here is a method on `Figment` rather than the `From` impl below
800
801        if providers.is_none() {
802            return Figment::from(self);
803        }
804
805        let root = self.root.as_path();
806        let profile = Self::selected_profile();
807        let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
808
809        // merge global foundry.toml file
810        if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
811            figment = Self::merge_toml_provider(
812                figment,
813                TomlFileProvider::new(None, global_toml),
814                profile.clone(),
815            );
816        }
817        // merge local foundry.toml file
818        figment = Self::merge_toml_provider(
819            figment,
820            TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
821            profile.clone(),
822        );
823
824        // merge environment variables
825        figment = figment
826            .merge(
827                Env::prefixed("DAPP_")
828                    .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
829                    .global(),
830            )
831            .merge(
832                Env::prefixed("DAPP_TEST_")
833                    .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
834                    .global(),
835            )
836            .merge(DappEnvCompatProvider)
837            .merge(EtherscanEnvProvider::default())
838            .merge(
839                Env::prefixed("FOUNDRY_")
840                    .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
841                    .map(|key| {
842                        let key = key.as_str();
843                        if Self::STANDALONE_SECTIONS.iter().any(|section| {
844                            key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
845                        }) {
846                            key.replacen('_', ".", 1).into()
847                        } else {
848                            key.into()
849                        }
850                    })
851                    .global(),
852            )
853            .select(profile.clone());
854
855        // only resolve remappings if all providers are requested
856        if providers.is_all() {
857            // we try to merge remappings after we've merged all other providers, this prevents
858            // redundant fs lookups to determine the default remappings that are eventually updated
859            // by other providers, like the toml file
860            let remappings = RemappingsProvider {
861                auto_detect_remappings: figment
862                    .extract_inner::<bool>("auto_detect_remappings")
863                    .unwrap_or(true),
864                lib_paths: figment
865                    .extract_inner::<Vec<PathBuf>>("libs")
866                    .map(Cow::Owned)
867                    .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
868                root,
869                remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
870            };
871            figment = figment.merge(remappings);
872        }
873
874        // normalize defaults
875        figment = self.normalize_defaults(figment);
876
877        Figment::from(self).merge(figment).select(profile)
878    }
879
880    /// The config supports relative paths and tracks the root path separately see
881    /// `Config::with_root`
882    ///
883    /// This joins all relative paths with the current root and attempts to make them canonic
884    #[must_use]
885    pub fn canonic(self) -> Self {
886        let root = self.root.clone();
887        self.canonic_at(root)
888    }
889
890    /// Joins all relative paths with the given root so that paths that are defined as:
891    ///
892    /// ```toml
893    /// [profile.default]
894    /// src = "src"
895    /// out = "./out"
896    /// libs = ["lib", "/var/lib"]
897    /// ```
898    ///
899    /// Will be made canonic with the given root:
900    ///
901    /// ```toml
902    /// [profile.default]
903    /// src = "<root>/src"
904    /// out = "<root>/out"
905    /// libs = ["<root>/lib", "/var/lib"]
906    /// ```
907    #[must_use]
908    pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
909        let root = canonic(root);
910
911        fn p(root: &Path, rem: &Path) -> PathBuf {
912            canonic(root.join(rem))
913        }
914
915        self.src = p(&root, &self.src);
916        self.test = p(&root, &self.test);
917        self.script = p(&root, &self.script);
918        self.out = p(&root, &self.out);
919        self.broadcast = p(&root, &self.broadcast);
920        self.cache_path = p(&root, &self.cache_path);
921        self.snapshots = p(&root, &self.snapshots);
922        self.test_failures_file = p(&root, &self.test_failures_file);
923
924        if let Some(build_info_path) = self.build_info_path {
925            self.build_info_path = Some(p(&root, &build_info_path));
926        }
927
928        self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
929
930        self.remappings =
931            self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
932
933        self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
934
935        self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
936
937        self.fs_permissions.join_all(&root);
938
939        if let Some(model_checker) = &mut self.model_checker {
940            model_checker.contracts = std::mem::take(&mut model_checker.contracts)
941                .into_iter()
942                .map(|(path, contracts)| {
943                    (format!("{}", p(&root, path.as_ref()).display()), contracts)
944                })
945                .collect();
946        }
947
948        self
949    }
950
951    /// Normalizes the evm version if a [SolcReq] is set
952    pub fn normalized_evm_version(mut self) -> Self {
953        self.normalize_evm_version();
954        self
955    }
956
957    /// Normalizes optimizer settings.
958    /// See <https://github.com/foundry-rs/foundry/issues/9665>
959    pub fn normalized_optimizer_settings(mut self) -> Self {
960        self.normalize_optimizer_settings();
961        self
962    }
963
964    /// Normalizes the evm version if a [SolcReq] is set to a valid version.
965    pub fn normalize_evm_version(&mut self) {
966        self.evm_version = self.get_normalized_evm_version();
967    }
968
969    /// Normalizes optimizer settings:
970    /// - with default settings, optimizer is set to false and optimizer runs to 200
971    /// - if optimizer is set and optimizer runs not specified, then optimizer runs is set to 200
972    /// - enable optimizer if not explicitly set and optimizer runs set to a value greater than 0
973    pub fn normalize_optimizer_settings(&mut self) {
974        match (self.optimizer, self.optimizer_runs) {
975            // Default: set the optimizer to false and optimizer runs to 200.
976            (None, None) => {
977                self.optimizer = Some(false);
978                self.optimizer_runs = Some(200);
979            }
980            // Set the optimizer runs to 200 if the `optimizer` config set.
981            (Some(_), None) => self.optimizer_runs = Some(200),
982            // Enables optimizer if the `optimizer_runs` has been set with a value greater than 0.
983            (None, Some(runs)) => self.optimizer = Some(runs > 0),
984            _ => {}
985        }
986    }
987
988    /// Returns the normalized [EvmVersion] for the current solc version, or the configured one.
989    pub fn get_normalized_evm_version(&self) -> EvmVersion {
990        if let Some(version) = self.solc_version()
991            && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
992        {
993            return evm_version;
994        }
995        self.evm_version
996    }
997
998    /// Returns a sanitized version of the Config where are paths are set correctly and potential
999    /// duplicates are resolved
1000    ///
1001    /// See [`Self::canonic`]
1002    #[must_use]
1003    pub fn sanitized(self) -> Self {
1004        let mut config = self.canonic();
1005
1006        config.sanitize_remappings();
1007
1008        config.libs.sort_unstable();
1009        config.libs.dedup();
1010
1011        config
1012    }
1013
1014    /// Cleans up any duplicate `Remapping` and sorts them
1015    ///
1016    /// On windows this will convert any `\` in the remapping path into a `/`
1017    pub fn sanitize_remappings(&mut self) {
1018        #[cfg(target_os = "windows")]
1019        {
1020            // force `/` in remappings on windows
1021            use path_slash::PathBufExt;
1022            self.remappings.iter_mut().for_each(|r| {
1023                r.path.path = r.path.path.to_slash_lossy().into_owned().into();
1024            });
1025        }
1026    }
1027
1028    /// Returns the directory in which dependencies should be installed
1029    ///
1030    /// Returns the first dir from `libs` that is not `node_modules` or `lib` if `libs` is empty
1031    pub fn install_lib_dir(&self) -> &Path {
1032        self.libs
1033            .iter()
1034            .find(|p| !p.ends_with("node_modules"))
1035            .map(|p| p.as_path())
1036            .unwrap_or_else(|| Path::new("lib"))
1037    }
1038
1039    /// Serves as the entrypoint for obtaining the project.
1040    ///
1041    /// Returns the `Project` configured with all `solc` and path related values.
1042    ///
1043    /// *Note*: this also _cleans_ [`Project::cleanup`] the workspace if `force` is set to true.
1044    ///
1045    /// # Example
1046    ///
1047    /// ```
1048    /// use foundry_config::Config;
1049    /// let config = Config::load_with_root(".")?.sanitized();
1050    /// let project = config.project()?;
1051    /// # Ok::<_, eyre::Error>(())
1052    /// ```
1053    pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1054        self.create_project(self.cache, false)
1055    }
1056
1057    /// Same as [`Self::project()`] but sets configures the project to not emit artifacts and ignore
1058    /// cache.
1059    pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1060        self.create_project(false, true)
1061    }
1062
1063    /// A cached, in-memory project that does not request any artifacts.
1064    ///
1065    /// Use this when you just want the source graph or the Solar compiler context.
1066    pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1067        let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
1068        let mut project = self.create_project(self.cache && !ui_testing, false)?;
1069        project.update_output_selection(|selection| {
1070            // We have to request something to populate `contracts` in the output and thus
1071            // artifacts.
1072            *selection = OutputSelection::common_output_selection(["abi".into()]);
1073        });
1074        Ok(project)
1075    }
1076
1077    /// Builds mapping with additional settings profiles.
1078    fn additional_settings(
1079        &self,
1080        base: &MultiCompilerSettings,
1081    ) -> BTreeMap<String, MultiCompilerSettings> {
1082        let mut map = BTreeMap::new();
1083
1084        for profile in &self.additional_compiler_profiles {
1085            let mut settings = base.clone();
1086            profile.apply(&mut settings);
1087            map.insert(profile.name.clone(), settings);
1088        }
1089
1090        map
1091    }
1092
1093    /// Resolves globs and builds a mapping from individual source files to their restrictions
1094    #[expect(clippy::disallowed_macros)]
1095    fn restrictions(
1096        &self,
1097        paths: &ProjectPathsConfig,
1098    ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
1099    {
1100        let mut map = BTreeMap::new();
1101        if self.compilation_restrictions.is_empty() {
1102            return Ok(BTreeMap::new());
1103        }
1104
1105        let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1106        let (sources, _) = graph.into_sources();
1107
1108        for res in &self.compilation_restrictions {
1109            for source in sources.keys().filter(|path| {
1110                if res.paths.is_match(path) {
1111                    true
1112                } else if let Ok(path) = path.strip_prefix(&paths.root) {
1113                    res.paths.is_match(path)
1114                } else {
1115                    false
1116                }
1117            }) {
1118                let res: RestrictionsWithVersion<_> =
1119                    res.clone().try_into().map_err(SolcError::msg)?;
1120                if !map.contains_key(source) {
1121                    map.insert(source.clone(), res);
1122                } else {
1123                    let value = map.remove(source.as_path()).unwrap();
1124                    if let Some(merged) = value.clone().merge(res) {
1125                        map.insert(source.clone(), merged);
1126                    } else {
1127                        // `sh_warn!` is a circular dependency, preventing us from using it here.
1128                        eprintln!(
1129                            "{}",
1130                            yansi::Paint::yellow(&format!(
1131                                "Failed to merge compilation restrictions for {}",
1132                                source.display()
1133                            ))
1134                        );
1135                        map.insert(source.clone(), value);
1136                    }
1137                }
1138            }
1139        }
1140
1141        Ok(map)
1142    }
1143
1144    /// Creates a [`Project`] with the given `cached` and `no_artifacts` flags.
1145    ///
1146    /// Prefer using [`Self::project`] or [`Self::ephemeral_project`] instead.
1147    pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1148        let settings = self.compiler_settings()?;
1149        let paths = self.project_paths();
1150        let mut builder = Project::builder()
1151            .artifacts(self.configured_artifacts_handler())
1152            .additional_settings(self.additional_settings(&settings))
1153            .restrictions(self.restrictions(&paths)?)
1154            .settings(settings)
1155            .paths(paths)
1156            .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1157            .ignore_paths(
1158                self.ignored_file_paths
1159                    .iter()
1160                    .map(|path| {
1161                        // Strip "./" prefix for consistent path matching
1162                        path.strip_prefix("./").unwrap_or(path).to_path_buf()
1163                    })
1164                    .collect::<Vec<_>>(),
1165            )
1166            .set_compiler_severity_filter(if self.deny.warnings() {
1167                Severity::Warning
1168            } else {
1169                Severity::Error
1170            })
1171            .set_offline(self.offline)
1172            .set_cached(cached)
1173            .set_build_info(!no_artifacts && self.build_info)
1174            .set_no_artifacts(no_artifacts);
1175
1176        if !self.skip.is_empty() {
1177            let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1178            builder = builder.sparse_output(filter);
1179        }
1180
1181        let project = builder.build(self.compiler()?)?;
1182
1183        if self.force {
1184            self.cleanup(&project)?;
1185        }
1186
1187        Ok(project)
1188    }
1189
1190    /// Disables optimizations and enables viaIR with minimum optimization if `ir_minimum` is true.
1191    pub fn disable_optimizations(&self, project: &mut Project, ir_minimum: bool) {
1192        if ir_minimum {
1193            // Enable viaIR with minimum optimization: https://github.com/ethereum/solidity/issues/12533#issuecomment-1013073350
1194            // And also in new releases of Solidity: https://github.com/ethereum/solidity/issues/13972#issuecomment-1628632202
1195            project.settings.solc.settings = std::mem::take(&mut project.settings.solc.settings)
1196                .with_via_ir_minimum_optimization();
1197
1198            // Sanitize settings for solc 0.8.4 if version cannot be detected: https://github.com/foundry-rs/foundry/issues/9322
1199            // But keep the EVM version: https://github.com/ethereum/solidity/issues/15775
1200            let evm_version = project.settings.solc.evm_version;
1201            let version = self.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
1202            project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
1203            project.settings.solc.evm_version = evm_version;
1204        } else {
1205            project.settings.solc.optimizer.disable();
1206            project.settings.solc.optimizer.runs = None;
1207            project.settings.solc.optimizer.details = None;
1208            project.settings.solc.via_ir = None;
1209        }
1210    }
1211
1212    /// Cleans the project.
1213    pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1214        &self,
1215        project: &Project<C, T>,
1216    ) -> Result<(), SolcError> {
1217        project.cleanup()?;
1218
1219        // Remove last test run failures file.
1220        let _ = fs::remove_file(&self.test_failures_file);
1221
1222        // Remove fuzz and invariant cache directories.
1223        let remove_test_dir = |test_dir: &Option<PathBuf>| {
1224            if let Some(test_dir) = test_dir {
1225                let path = project.root().join(test_dir);
1226                if path.exists() {
1227                    let _ = fs::remove_dir_all(&path);
1228                }
1229            }
1230        };
1231        remove_test_dir(&self.fuzz.failure_persist_dir);
1232        remove_test_dir(&self.fuzz.corpus.corpus_dir);
1233        remove_test_dir(&self.invariant.corpus.corpus_dir);
1234        remove_test_dir(&self.invariant.failure_persist_dir);
1235
1236        Ok(())
1237    }
1238
1239    /// Ensures that the configured version is installed if explicitly set
1240    ///
1241    /// If `solc` is [`SolcReq::Version`] then this will download and install the solc version if
1242    /// it's missing, unless the `offline` flag is enabled, in which case an error is thrown.
1243    ///
1244    /// If `solc` is [`SolcReq::Local`] then this will ensure that the path exists.
1245    fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1246        if let Some(solc) = &self.solc {
1247            let solc = match solc {
1248                SolcReq::Version(version) => {
1249                    if let Some(solc) = Solc::find_svm_installed_version(version)? {
1250                        solc
1251                    } else {
1252                        if self.offline {
1253                            return Err(SolcError::msg(format!(
1254                                "can't install missing solc {version} in offline mode"
1255                            )));
1256                        }
1257                        Solc::blocking_install(version)?
1258                    }
1259                }
1260                SolcReq::Local(solc) => {
1261                    if !solc.is_file() {
1262                        return Err(SolcError::msg(format!(
1263                            "`solc` {} does not exist",
1264                            solc.display()
1265                        )));
1266                    }
1267                    Solc::new(solc)?
1268                }
1269            };
1270            return Ok(Some(solc));
1271        }
1272
1273        Ok(None)
1274    }
1275
1276    /// Returns the [SpecId] derived from the configured [EvmVersion]
1277    pub fn evm_spec_id(&self) -> SpecId {
1278        evm_spec_id(self.evm_version)
1279    }
1280
1281    /// Returns whether the compiler version should be auto-detected
1282    ///
1283    /// Returns `false` if `solc_version` is explicitly set, otherwise returns the value of
1284    /// `auto_detect_solc`
1285    pub fn is_auto_detect(&self) -> bool {
1286        if self.solc.is_some() {
1287            return false;
1288        }
1289        self.auto_detect_solc
1290    }
1291
1292    /// Whether caching should be enabled for the given chain id
1293    pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1294        !self.no_storage_caching
1295            && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1296            && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1297    }
1298
1299    /// Returns the `ProjectPathsConfig` sub set of the config.
1300    ///
1301    /// **NOTE**: this uses the paths as they are and does __not__ modify them, see
1302    /// `[Self::sanitized]`
1303    ///
1304    /// # Example
1305    ///
1306    /// ```
1307    /// use foundry_compilers::solc::Solc;
1308    /// use foundry_config::Config;
1309    /// let config = Config::load_with_root(".")?.sanitized();
1310    /// let paths = config.project_paths::<Solc>();
1311    /// # Ok::<_, eyre::Error>(())
1312    /// ```
1313    pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1314        let mut builder = ProjectPathsConfig::builder()
1315            .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1316            .sources(&self.src)
1317            .tests(&self.test)
1318            .scripts(&self.script)
1319            .artifacts(&self.out)
1320            .libs(self.libs.iter())
1321            .remappings(self.get_all_remappings())
1322            .allowed_path(&self.root)
1323            .allowed_paths(&self.libs)
1324            .allowed_paths(&self.allow_paths)
1325            .include_paths(&self.include_paths);
1326
1327        if let Some(build_info_path) = &self.build_info_path {
1328            builder = builder.build_infos(build_info_path);
1329        }
1330
1331        builder.build_with_root(&self.root)
1332    }
1333
1334    /// Returns configuration for a compiler to use when setting up a [Project].
1335    pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1336        if let Some(solc) = self.ensure_solc()? {
1337            Ok(SolcCompiler::Specific(solc))
1338        } else {
1339            Ok(SolcCompiler::AutoDetect)
1340        }
1341    }
1342
1343    /// Returns the solc version, if any.
1344    pub fn solc_version(&self) -> Option<Version> {
1345        self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1346    }
1347
1348    /// Returns configured [Vyper] compiler.
1349    pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1350        // Only instantiate Vyper if there are any Vyper files in the project.
1351        if !self.project_paths::<VyperLanguage>().has_input_files() {
1352            return Ok(None);
1353        }
1354        let vyper = if let Some(path) = &self.vyper.path {
1355            Some(Vyper::new(path)?)
1356        } else {
1357            Vyper::new("vyper").ok()
1358        };
1359        Ok(vyper)
1360    }
1361
1362    /// Returns configuration for a compiler to use when setting up a [Project].
1363    pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1364        Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1365    }
1366
1367    /// Returns configured [MultiCompilerSettings].
1368    pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1369        Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1370    }
1371
1372    /// Returns all configured remappings.
1373    pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1374        self.remappings.iter().map(|m| m.clone().into())
1375    }
1376
1377    /// Returns the configured rpc jwt secret
1378    ///
1379    /// Returns:
1380    ///    - The jwt secret, if configured
1381    ///
1382    /// # Example
1383    ///
1384    /// ```
1385    /// use foundry_config::Config;
1386    /// # fn t() {
1387    /// let config = Config::with_root("./");
1388    /// let rpc_jwt = config.get_rpc_jwt_secret().unwrap().unwrap();
1389    /// # }
1390    /// ```
1391    pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1392        Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1393    }
1394
1395    /// Returns the configured rpc url
1396    ///
1397    /// Returns:
1398    ///    - the matching, resolved url of  `rpc_endpoints` if `eth_rpc_url` is an alias
1399    ///    - the `eth_rpc_url` as-is if it isn't an alias
1400    ///
1401    /// # Example
1402    ///
1403    /// ```
1404    /// use foundry_config::Config;
1405    /// # fn t() {
1406    /// let config = Config::with_root("./");
1407    /// let rpc_url = config.get_rpc_url().unwrap().unwrap();
1408    /// # }
1409    /// ```
1410    pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1411        let maybe_alias = self.eth_rpc_url.as_deref()?;
1412        if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1413            Some(alias)
1414        } else {
1415            Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1416        }
1417    }
1418
1419    /// Resolves the given alias to a matching rpc url
1420    ///
1421    /// # Returns
1422    ///
1423    /// In order of resolution:
1424    ///
1425    /// - the matching, resolved url of `rpc_endpoints` if `maybe_alias` is an alias
1426    /// - a mesc resolved url if `maybe_alias` is a known alias in mesc
1427    /// - `None` otherwise
1428    ///
1429    /// # Note on mesc
1430    ///
1431    /// The endpoint is queried for in mesc under the `foundry` profile, allowing users to customize
1432    /// endpoints for Foundry specifically.
1433    ///
1434    /// # Example
1435    ///
1436    /// ```
1437    /// use foundry_config::Config;
1438    /// # fn t() {
1439    /// let config = Config::with_root("./");
1440    /// let rpc_url = config.get_rpc_url_with_alias("mainnet").unwrap().unwrap();
1441    /// # }
1442    /// ```
1443    pub fn get_rpc_url_with_alias(
1444        &self,
1445        maybe_alias: &str,
1446    ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1447        let mut endpoints = self.rpc_endpoints.clone().resolved();
1448        if let Some(endpoint) = endpoints.remove(maybe_alias) {
1449            return Some(endpoint.url().map(Cow::Owned));
1450        }
1451
1452        if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1453            return Some(Ok(Cow::Owned(mesc_url)));
1454        }
1455
1456        None
1457    }
1458
1459    /// Attempts to resolve the URL for the given alias from [`mesc`](https://github.com/paradigmxyz/mesc)
1460    pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1461        // Note: mesc requires a MESC_PATH in the env, which the user can configure and is expected
1462        // to be part of the shell profile, default is ~/mesc.json
1463        let mesc_config = mesc::load::load_config_data()
1464            .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1465            .ok()?;
1466
1467        if let Ok(Some(endpoint)) =
1468            mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1469        {
1470            return Some(endpoint.url);
1471        }
1472
1473        if maybe_alias.chars().all(|c| c.is_numeric()) {
1474            // try to lookup the mesc network by chain id if alias is numeric
1475            // This only succeeds if the chain id has a default:
1476            // "network_defaults": {
1477            //    "50104": "sophon_50104"
1478            // }
1479            if let Ok(Some(endpoint)) =
1480                mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1481            {
1482                return Some(endpoint.url);
1483            }
1484        }
1485
1486        None
1487    }
1488
1489    /// Returns the configured rpc, or the fallback url
1490    ///
1491    /// # Example
1492    ///
1493    /// ```
1494    /// use foundry_config::Config;
1495    /// # fn t() {
1496    /// let config = Config::with_root("./");
1497    /// let rpc_url = config.get_rpc_url_or("http://localhost:8545").unwrap();
1498    /// # }
1499    /// ```
1500    pub fn get_rpc_url_or<'a>(
1501        &'a self,
1502        fallback: impl Into<Cow<'a, str>>,
1503    ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1504        if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1505    }
1506
1507    /// Returns the configured rpc or `"http://localhost:8545"` if no `eth_rpc_url` is set
1508    ///
1509    /// # Example
1510    ///
1511    /// ```
1512    /// use foundry_config::Config;
1513    /// # fn t() {
1514    /// let config = Config::with_root("./");
1515    /// let rpc_url = config.get_rpc_url_or_localhost_http().unwrap();
1516    /// # }
1517    /// ```
1518    pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1519        self.get_rpc_url_or("http://localhost:8545")
1520    }
1521
1522    /// Returns the `EtherscanConfig` to use, if any
1523    ///
1524    /// Returns
1525    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if `etherscan_api_key` is
1526    ///    an alias
1527    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if a `chain` is
1528    ///    configured. an alias
1529    ///  - the Mainnet  `ResolvedEtherscanConfig` if `etherscan_api_key` is set, `None` otherwise
1530    ///
1531    /// # Example
1532    ///
1533    /// ```
1534    /// use foundry_config::Config;
1535    /// # fn t() {
1536    /// let config = Config::with_root("./");
1537    /// let etherscan_config = config.get_etherscan_config().unwrap().unwrap();
1538    /// let client = etherscan_config.into_client().unwrap();
1539    /// # }
1540    /// ```
1541    pub fn get_etherscan_config(
1542        &self,
1543    ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1544        self.get_etherscan_config_with_chain(None).transpose()
1545    }
1546
1547    /// Same as [`Self::get_etherscan_config()`] but optionally updates the config with the given
1548    /// `chain`, and `etherscan_api_key`
1549    ///
1550    /// If not matching alias was found, then this will try to find the first entry in the table
1551    /// with a matching chain id. If an etherscan_api_key is already set it will take precedence
1552    /// over the chain's entry in the table.
1553    pub fn get_etherscan_config_with_chain(
1554        &self,
1555        chain: Option<Chain>,
1556    ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1557        if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1558            && self.etherscan.contains_key(maybe_alias)
1559        {
1560            return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
1561        }
1562
1563        // try to find by comparing chain IDs after resolving
1564        if let Some(res) = chain
1565            .or(self.chain)
1566            .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1567        {
1568            match (res, self.etherscan_api_key.as_ref()) {
1569                (Ok(mut config), Some(key)) => {
1570                    // we update the key, because if an etherscan_api_key is set, it should take
1571                    // precedence over the entry, since this is usually set via env var or CLI args.
1572                    config.key.clone_from(key);
1573                    return Ok(Some(config));
1574                }
1575                (Ok(config), None) => return Ok(Some(config)),
1576                (Err(err), None) => return Err(err),
1577                (Err(_), Some(_)) => {
1578                    // use the etherscan key as fallback
1579                }
1580            }
1581        }
1582
1583        // etherscan fallback via API key
1584        if let Some(key) = self.etherscan_api_key.as_ref() {
1585            return Ok(ResolvedEtherscanConfig::create(
1586                key,
1587                chain.or(self.chain).unwrap_or_default(),
1588            ));
1589        }
1590        Ok(None)
1591    }
1592
1593    /// Helper function to just get the API key
1594    ///
1595    /// Optionally updates the config with the given `chain`.
1596    ///
1597    /// See also [Self::get_etherscan_config_with_chain]
1598    pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1599        self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1600    }
1601
1602    /// Returns the remapping for the project's _src_ directory
1603    ///
1604    /// **Note:** this will add an additional `<src>/=<src path>` remapping here so imports that
1605    /// look like `import {Foo} from "src/Foo.sol";` are properly resolved.
1606    ///
1607    /// This is due the fact that `solc`'s VFS resolves [direct imports](https://docs.soliditylang.org/en/develop/path-resolution.html#direct-imports) that start with the source directory's name.
1608    pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1609        get_dir_remapping(&self.src)
1610    }
1611
1612    /// Returns the remapping for the project's _test_ directory, but only if it exists
1613    pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1614        if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1615    }
1616
1617    /// Returns the remapping for the project's _script_ directory, but only if it exists
1618    pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1619        if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1620    }
1621
1622    /// Returns the `Optimizer` based on the configured settings
1623    ///
1624    /// Note: optimizer details can be set independently of `enabled`
1625    /// See also: <https://github.com/foundry-rs/foundry/issues/7689>
1626    /// and  <https://github.com/ethereum/solidity/blob/bbb7f58be026fdc51b0b4694a6f25c22a1425586/docs/using-the-compiler.rst?plain=1#L293-L294>
1627    pub fn optimizer(&self) -> Optimizer {
1628        Optimizer {
1629            enabled: self.optimizer,
1630            runs: self.optimizer_runs,
1631            // we always set the details because `enabled` is effectively a specific details profile
1632            // that can still be modified
1633            details: self.optimizer_details.clone(),
1634        }
1635    }
1636
1637    /// returns the [`foundry_compilers::ConfigurableArtifacts`] for this config, that includes the
1638    /// `extra_output` fields
1639    pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1640        let mut extra_output = self.extra_output.clone();
1641
1642        // Sourcify verification requires solc metadata output. Since, it doesn't
1643        // affect the UX & performance of the compiler, output the metadata files
1644        // by default.
1645        // For more info see: <https://github.com/foundry-rs/foundry/issues/2795>
1646        // Metadata is not emitted as separate file because this breaks typechain support: <https://github.com/foundry-rs/foundry/issues/2969>
1647        if !extra_output.contains(&ContractOutputSelection::Metadata) {
1648            extra_output.push(ContractOutputSelection::Metadata);
1649        }
1650
1651        ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1652    }
1653
1654    /// Parses all libraries in the form of
1655    /// `<file>:<lib>:<addr>`
1656    pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1657        Libraries::parse(&self.libraries)
1658    }
1659
1660    /// Returns all libraries with applied remappings. Same as `self.solc_settings()?.libraries`.
1661    pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1662        let paths: ProjectPathsConfig = self.project_paths();
1663        Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1664    }
1665
1666    /// Returns the configured `solc` `Settings` that includes:
1667    /// - all libraries
1668    /// - the optimizer (including details, if configured)
1669    /// - evm version
1670    pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1671        // By default if no targets are specifically selected the model checker uses all targets.
1672        // This might be too much here, so only enable assertion checks.
1673        // If users wish to enable all options they need to do so explicitly.
1674        let mut model_checker = self.model_checker.clone();
1675        if let Some(model_checker_settings) = &mut model_checker
1676            && model_checker_settings.targets.is_none()
1677        {
1678            model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1679        }
1680
1681        let mut settings = Settings {
1682            libraries: self.libraries_with_remappings()?,
1683            optimizer: self.optimizer(),
1684            evm_version: Some(self.evm_version),
1685            metadata: Some(SettingsMetadata {
1686                use_literal_content: Some(self.use_literal_content),
1687                bytecode_hash: Some(self.bytecode_hash),
1688                cbor_metadata: Some(self.cbor_metadata),
1689            }),
1690            debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1691                revert_strings: Some(revert_strings),
1692                // Not used.
1693                debug_info: Vec::new(),
1694            }),
1695            model_checker,
1696            via_ir: Some(self.via_ir),
1697            // Not used.
1698            stop_after: None,
1699            // Set in project paths.
1700            remappings: Vec::new(),
1701            // Set with `with_extra_output` below.
1702            output_selection: Default::default(),
1703        }
1704        .with_extra_output(self.configured_artifacts_handler().output_selection());
1705
1706        // We're keeping AST in `--build-info` for backwards compatibility with HardHat.
1707        if self.ast || self.build_info {
1708            settings = settings.with_ast();
1709        }
1710
1711        let cli_settings =
1712            CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1713
1714        Ok(SolcSettings { settings, cli_settings })
1715    }
1716
1717    /// Returns the configured [VyperSettings] that includes:
1718    /// - evm version
1719    pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1720        Ok(VyperSettings {
1721            evm_version: Some(self.evm_version),
1722            optimize: self.vyper.optimize,
1723            bytecode_metadata: None,
1724            // TODO: We don't yet have a way to deserialize other outputs correctly, so request only
1725            // those for now. It should be enough to run tests and deploy contracts.
1726            output_selection: OutputSelection::common_output_selection([
1727                "abi".to_string(),
1728                "evm.bytecode".to_string(),
1729                "evm.deployedBytecode".to_string(),
1730            ]),
1731            search_paths: None,
1732            experimental_codegen: self.vyper.experimental_codegen,
1733        })
1734    }
1735
1736    /// Returns the default figment
1737    ///
1738    /// The default figment reads from the following sources, in ascending
1739    /// priority order:
1740    ///
1741    ///   1. [`Config::default()`] (see [defaults](#defaults))
1742    ///   2. `foundry.toml` _or_ filename in `FOUNDRY_CONFIG` environment variable
1743    ///   3. `FOUNDRY_` prefixed environment variables
1744    ///
1745    /// The profile selected is the value set in the `FOUNDRY_PROFILE`
1746    /// environment variable. If it is not set, it defaults to `default`.
1747    ///
1748    /// # Example
1749    ///
1750    /// ```rust
1751    /// use foundry_config::Config;
1752    /// use serde::Deserialize;
1753    ///
1754    /// let my_config = Config::figment().extract::<Config>();
1755    /// ```
1756    pub fn figment() -> Figment {
1757        Self::default().into()
1758    }
1759
1760    /// Returns the default figment enhanced with additional context extracted from the provided
1761    /// root, like remappings and directories.
1762    ///
1763    /// # Example
1764    ///
1765    /// ```rust
1766    /// use foundry_config::Config;
1767    /// use serde::Deserialize;
1768    ///
1769    /// let my_config = Config::figment_with_root(".").extract::<Config>();
1770    /// ```
1771    pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1772        Self::with_root(root.as_ref()).into()
1773    }
1774
1775    #[doc(hidden)]
1776    #[track_caller]
1777    pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1778        let root = match root {
1779            Some(root) => root,
1780            None => &find_project_root(None).expect("could not determine project root"),
1781        };
1782        Self::figment_with_root(root)
1783    }
1784
1785    /// Creates a new Config that adds additional context extracted from the provided root.
1786    ///
1787    /// # Example
1788    ///
1789    /// ```rust
1790    /// use foundry_config::Config;
1791    /// let my_config = Config::with_root(".");
1792    /// ```
1793    pub fn with_root(root: impl AsRef<Path>) -> Self {
1794        Self::_with_root(root.as_ref())
1795    }
1796
1797    fn _with_root(root: &Path) -> Self {
1798        // autodetect paths
1799        let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1800        let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1801        Self {
1802            root: paths.root,
1803            src: paths.sources.file_name().unwrap().into(),
1804            out: artifacts.clone(),
1805            libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1806            remappings: paths
1807                .remappings
1808                .into_iter()
1809                .map(|r| RelativeRemapping::new(r, root))
1810                .collect(),
1811            fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1812            ..Self::default()
1813        }
1814    }
1815
1816    /// Returns the default config but with hardhat paths
1817    pub fn hardhat() -> Self {
1818        Self {
1819            src: "contracts".into(),
1820            out: "artifacts".into(),
1821            libs: vec!["node_modules".into()],
1822            ..Self::default()
1823        }
1824    }
1825
1826    /// Extracts a basic subset of the config, used for initialisations.
1827    ///
1828    /// # Example
1829    ///
1830    /// ```rust
1831    /// use foundry_config::Config;
1832    /// let my_config = Config::with_root(".").into_basic();
1833    /// ```
1834    pub fn into_basic(self) -> BasicConfig {
1835        BasicConfig {
1836            profile: self.profile,
1837            src: self.src,
1838            out: self.out,
1839            libs: self.libs,
1840            remappings: self.remappings,
1841        }
1842    }
1843
1844    /// Updates the `foundry.toml` file for the given `root` based on the provided closure.
1845    ///
1846    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1847    /// [Self::get_config_path()] and if the closure returns `true`.
1848    pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1849    where
1850        F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1851    {
1852        let config = Self::load_with_root(root)?.sanitized();
1853        config.update(|doc| f(&config, doc))
1854    }
1855
1856    /// Updates the `foundry.toml` file this `Config` ias based on with the provided closure.
1857    ///
1858    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1859    /// [Self::get_config_path()] and if the closure returns `true`
1860    pub fn update<F>(&self, f: F) -> eyre::Result<()>
1861    where
1862        F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1863    {
1864        let file_path = self.get_config_path();
1865        if !file_path.exists() {
1866            return Ok(());
1867        }
1868        let contents = fs::read_to_string(&file_path)?;
1869        let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1870        if f(&mut doc) {
1871            fs::write(file_path, doc.to_string())?;
1872        }
1873        Ok(())
1874    }
1875
1876    /// Sets the `libs` entry inside a `foundry.toml` file but only if it exists
1877    ///
1878    /// # Errors
1879    ///
1880    /// An error if the `foundry.toml` could not be parsed.
1881    pub fn update_libs(&self) -> eyre::Result<()> {
1882        self.update(|doc| {
1883            let profile = self.profile.as_str().as_str();
1884            let root = &self.root;
1885            let libs: toml_edit::Value = self
1886                .libs
1887                .iter()
1888                .map(|path| {
1889                    let path =
1890                        if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1891                    toml_edit::Value::from(&*path.to_string_lossy())
1892                })
1893                .collect();
1894            let libs = toml_edit::value(libs);
1895            doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1896            true
1897        })
1898    }
1899
1900    /// Serialize the config type as a String of TOML.
1901    ///
1902    /// This serializes to a table with the name of the profile
1903    ///
1904    /// ```toml
1905    /// [profile.default]
1906    /// src = "src"
1907    /// out = "out"
1908    /// libs = ["lib"]
1909    /// # ...
1910    /// ```
1911    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1912        // serializing to value first to prevent `ValueAfterTable` errors
1913        let mut value = toml::Value::try_from(self)?;
1914        // Config map always gets serialized as a table
1915        let value_table = value.as_table_mut().unwrap();
1916        // remove standalone sections from inner table
1917        let standalone_sections = Self::STANDALONE_SECTIONS
1918            .iter()
1919            .filter_map(|section| {
1920                let section = section.to_string();
1921                value_table.remove(&section).map(|value| (section, value))
1922            })
1923            .collect::<Vec<_>>();
1924        // wrap inner table in [profile.<profile>]
1925        let mut wrapping_table = [(
1926            Self::PROFILE_SECTION.into(),
1927            toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1928        )]
1929        .into_iter()
1930        .collect::<toml::map::Map<_, _>>();
1931        // insert standalone sections
1932        for (section, value) in standalone_sections {
1933            wrapping_table.insert(section, value);
1934        }
1935        // stringify
1936        toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1937    }
1938
1939    /// Returns the path to the `foundry.toml` of this `Config`.
1940    pub fn get_config_path(&self) -> PathBuf {
1941        self.root.join(Self::FILE_NAME)
1942    }
1943
1944    /// Returns the selected profile.
1945    ///
1946    /// If the `FOUNDRY_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`.
1947    pub fn selected_profile() -> Profile {
1948        // Can't cache in tests because the env var can change.
1949        #[cfg(test)]
1950        {
1951            Self::force_selected_profile()
1952        }
1953        #[cfg(not(test))]
1954        {
1955            static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
1956            CACHE.get_or_init(Self::force_selected_profile).clone()
1957        }
1958    }
1959
1960    fn force_selected_profile() -> Profile {
1961        Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1962    }
1963
1964    /// Returns the path to foundry's global TOML file: `~/.foundry/foundry.toml`.
1965    pub fn foundry_dir_toml() -> Option<PathBuf> {
1966        Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1967    }
1968
1969    /// Returns the path to foundry's config dir: `~/.foundry/`.
1970    pub fn foundry_dir() -> Option<PathBuf> {
1971        dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1972    }
1973
1974    /// Returns the path to foundry's cache dir: `~/.foundry/cache`.
1975    pub fn foundry_cache_dir() -> Option<PathBuf> {
1976        Self::foundry_dir().map(|p| p.join("cache"))
1977    }
1978
1979    /// Returns the path to foundry rpc cache dir: `~/.foundry/cache/rpc`.
1980    pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1981        Some(Self::foundry_cache_dir()?.join("rpc"))
1982    }
1983    /// Returns the path to foundry chain's cache dir: `~/.foundry/cache/rpc/<chain>`
1984    pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1985        Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1986    }
1987
1988    /// Returns the path to foundry's etherscan cache dir: `~/.foundry/cache/etherscan`.
1989    pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1990        Some(Self::foundry_cache_dir()?.join("etherscan"))
1991    }
1992
1993    /// Returns the path to foundry's keystores dir: `~/.foundry/keystores`.
1994    pub fn foundry_keystores_dir() -> Option<PathBuf> {
1995        Some(Self::foundry_dir()?.join("keystores"))
1996    }
1997
1998    /// Returns the path to foundry's etherscan cache dir for `chain_id`:
1999    /// `~/.foundry/cache/etherscan/<chain>`
2000    pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2001        Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
2002    }
2003
2004    /// Returns the path to the cache dir of the `block` on the `chain`:
2005    /// `~/.foundry/cache/rpc/<chain>/<block>`
2006    pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2007        Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
2008    }
2009
2010    /// Returns the path to the cache file of the `block` on the `chain`:
2011    /// `~/.foundry/cache/rpc/<chain>/<block>/storage.json`
2012    pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2013        Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
2014    }
2015
2016    /// Returns the path to `foundry`'s data directory inside the user's data directory.
2017    ///
2018    /// | Platform | Value                                         | Example                                          |
2019    /// | -------  | --------------------------------------------- | ------------------------------------------------ |
2020    /// | Linux    | `$XDG_CONFIG_HOME` or `$HOME`/.config/foundry | /home/alice/.config/foundry                      |
2021    /// | macOS    | `$HOME`/Library/Application Support/foundry   | /Users/Alice/Library/Application Support/foundry |
2022    /// | Windows  | `{FOLDERID_RoamingAppData}/foundry`           | C:\Users\Alice\AppData\Roaming/foundry           |
2023    pub fn data_dir() -> eyre::Result<PathBuf> {
2024        let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
2025        std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
2026        Ok(path)
2027    }
2028
2029    /// Returns the path to the `foundry.toml` file, the file is searched for in
2030    /// the current working directory and all parent directories until the root,
2031    /// and the first hit is used.
2032    ///
2033    /// If this search comes up empty, then it checks if a global `foundry.toml` exists at
2034    /// `~/.foundry/foundry.toml`, see [`Self::foundry_dir_toml`].
2035    pub fn find_config_file() -> Option<PathBuf> {
2036        fn find(path: &Path) -> Option<PathBuf> {
2037            if path.is_absolute() {
2038                return match path.is_file() {
2039                    true => Some(path.to_path_buf()),
2040                    false => None,
2041                };
2042            }
2043            let cwd = std::env::current_dir().ok()?;
2044            let mut cwd = cwd.as_path();
2045            loop {
2046                let file_path = cwd.join(path);
2047                if file_path.is_file() {
2048                    return Some(file_path);
2049                }
2050                cwd = cwd.parent()?;
2051            }
2052        }
2053        find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
2054            .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
2055    }
2056
2057    /// Clears the foundry cache.
2058    pub fn clean_foundry_cache() -> eyre::Result<()> {
2059        if let Some(cache_dir) = Self::foundry_cache_dir() {
2060            let path = cache_dir.as_path();
2061            let _ = fs::remove_dir_all(path);
2062        } else {
2063            eyre::bail!("failed to get foundry_cache_dir");
2064        }
2065
2066        Ok(())
2067    }
2068
2069    /// Clears the foundry cache for `chain`.
2070    pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
2071        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2072            let path = cache_dir.as_path();
2073            let _ = fs::remove_dir_all(path);
2074        } else {
2075            eyre::bail!("failed to get foundry_chain_cache_dir");
2076        }
2077
2078        Ok(())
2079    }
2080
2081    /// Clears the foundry cache for `chain` and `block`.
2082    pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
2083        if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2084            let path = cache_dir.as_path();
2085            let _ = fs::remove_dir_all(path);
2086        } else {
2087            eyre::bail!("failed to get foundry_block_cache_dir");
2088        }
2089
2090        Ok(())
2091    }
2092
2093    /// Clears the foundry etherscan cache.
2094    pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
2095        if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2096            let path = cache_dir.as_path();
2097            let _ = fs::remove_dir_all(path);
2098        } else {
2099            eyre::bail!("failed to get foundry_etherscan_cache_dir");
2100        }
2101
2102        Ok(())
2103    }
2104
2105    /// Clears the foundry etherscan cache for `chain`.
2106    pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
2107        if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2108            let path = cache_dir.as_path();
2109            let _ = fs::remove_dir_all(path);
2110        } else {
2111            eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2112        }
2113
2114        Ok(())
2115    }
2116
2117    /// List the data in the foundry cache.
2118    pub fn list_foundry_cache() -> eyre::Result<Cache> {
2119        if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2120            let mut cache = Cache { chains: vec![] };
2121            if !cache_dir.exists() {
2122                return Ok(cache);
2123            }
2124            if let Ok(entries) = cache_dir.as_path().read_dir() {
2125                for entry in entries.flatten().filter(|x| x.path().is_dir()) {
2126                    match Chain::from_str(&entry.file_name().to_string_lossy()) {
2127                        Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
2128                        Err(_) => continue,
2129                    }
2130                }
2131                Ok(cache)
2132            } else {
2133                eyre::bail!("failed to access foundry_cache_dir");
2134            }
2135        } else {
2136            eyre::bail!("failed to get foundry_cache_dir");
2137        }
2138    }
2139
2140    /// List the cached data for `chain`.
2141    pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2142        let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2143            Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2144            None => {
2145                warn!("failed to access foundry_etherscan_chain_cache_dir");
2146                0
2147            }
2148        };
2149
2150        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2151            let blocks = Self::get_cached_blocks(&cache_dir)?;
2152            Ok(ChainCache {
2153                name: chain.to_string(),
2154                blocks,
2155                block_explorer: block_explorer_data_size,
2156            })
2157        } else {
2158            eyre::bail!("failed to get foundry_chain_cache_dir");
2159        }
2160    }
2161
2162    /// The path provided to this function should point to a cached chain folder.
2163    fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2164        let mut blocks = vec![];
2165        if !chain_path.exists() {
2166            return Ok(blocks);
2167        }
2168        for block in chain_path.read_dir()?.flatten() {
2169            let file_type = block.file_type()?;
2170            let file_name = block.file_name();
2171            let filepath = if file_type.is_dir() {
2172                block.path().join("storage.json")
2173            } else if file_type.is_file()
2174                && file_name.to_string_lossy().chars().all(char::is_numeric)
2175            {
2176                block.path()
2177            } else {
2178                continue;
2179            };
2180            blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2181        }
2182        Ok(blocks)
2183    }
2184
2185    /// The path provided to this function should point to the etherscan cache for a chain.
2186    fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2187        if !chain_path.exists() {
2188            return Ok(0);
2189        }
2190
2191        fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2192            dir.try_fold(0, |acc, file| {
2193                let file = file?;
2194                let size = match file.metadata()? {
2195                    data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2196                    data => data.len(),
2197                };
2198                Ok(acc + size)
2199            })
2200        }
2201
2202        dir_size_recursive(fs::read_dir(chain_path)?)
2203    }
2204
2205    fn merge_toml_provider(
2206        mut figment: Figment,
2207        toml_provider: impl Provider,
2208        profile: Profile,
2209    ) -> Figment {
2210        figment = figment.select(profile.clone());
2211
2212        // add warnings
2213        figment = {
2214            let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2215            figment.merge(warnings)
2216        };
2217
2218        // use [profile.<profile>] as [<profile>]
2219        let mut profiles = vec![Self::DEFAULT_PROFILE];
2220        if profile != Self::DEFAULT_PROFILE {
2221            profiles.push(profile.clone());
2222        }
2223        let provider = toml_provider.strict_select(profiles);
2224
2225        // apply any key fixes
2226        let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2227
2228        // merge the default profile as a base
2229        if profile != Self::DEFAULT_PROFILE {
2230            figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2231        }
2232        // merge special keys into config
2233        for standalone_key in Self::STANDALONE_SECTIONS {
2234            if let Some((_, fallback)) =
2235                STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2236            {
2237                figment = figment.merge(
2238                    provider
2239                        .fallback(standalone_key, fallback)
2240                        .wrap(profile.clone(), standalone_key),
2241                );
2242            } else {
2243                figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2244            }
2245        }
2246        // merge the profile
2247        figment = figment.merge(provider);
2248        figment
2249    }
2250
2251    /// Check if any defaults need to be normalized.
2252    ///
2253    /// This normalizes the default `evm_version` if a `solc` was provided in the config.
2254    ///
2255    /// See also <https://github.com/foundry-rs/foundry/issues/7014>
2256    fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2257        if figment.contains("evm_version") {
2258            return figment;
2259        }
2260
2261        // Normalize `evm_version` based on the provided solc version.
2262        if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2263            && let Some(version) = solc
2264                .try_version()
2265                .ok()
2266                .and_then(|version| self.evm_version.normalize_version_solc(&version))
2267        {
2268            figment = figment.merge(("evm_version", version));
2269        }
2270
2271        // Normalize `deny` based on the provided `deny_warnings` version.
2272        if self.deny_warnings
2273            && let Ok(DenyLevel::Never) = figment.extract_inner("deny")
2274        {
2275            figment = figment.merge(("deny", DenyLevel::Warnings));
2276        }
2277
2278        figment
2279    }
2280}
2281
2282impl From<Config> for Figment {
2283    fn from(c: Config) -> Self {
2284        (&c).into()
2285    }
2286}
2287impl From<&Config> for Figment {
2288    fn from(c: &Config) -> Self {
2289        c.to_figment(FigmentProviders::All)
2290    }
2291}
2292
2293/// Determines what providers should be used when loading the [`Figment`] for a [`Config`].
2294#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2295pub enum FigmentProviders {
2296    /// Include all providers.
2297    #[default]
2298    All,
2299    /// Only include necessary providers that are useful for cast commands.
2300    ///
2301    /// This will exclude more expensive providers such as remappings.
2302    Cast,
2303    /// Only include necessary providers that are useful for anvil.
2304    ///
2305    /// This will exclude more expensive providers such as remappings.
2306    Anvil,
2307    /// Don't include any providers.
2308    None,
2309}
2310
2311impl FigmentProviders {
2312    /// Returns true if all providers should be included.
2313    pub const fn is_all(&self) -> bool {
2314        matches!(self, Self::All)
2315    }
2316
2317    /// Returns true if this is the cast preset.
2318    pub const fn is_cast(&self) -> bool {
2319        matches!(self, Self::Cast)
2320    }
2321
2322    /// Returns true if this is the anvil preset.
2323    pub const fn is_anvil(&self) -> bool {
2324        matches!(self, Self::Anvil)
2325    }
2326
2327    /// Returns true if no providers should be included.
2328    pub const fn is_none(&self) -> bool {
2329        matches!(self, Self::None)
2330    }
2331}
2332
2333/// Wrapper type for [`regex::Regex`] that implements [`PartialEq`] and [`serde`] traits.
2334#[derive(Clone, Debug, Serialize, Deserialize)]
2335#[serde(transparent)]
2336pub struct RegexWrapper {
2337    #[serde(with = "serde_regex")]
2338    inner: regex::Regex,
2339}
2340
2341impl std::ops::Deref for RegexWrapper {
2342    type Target = regex::Regex;
2343
2344    fn deref(&self) -> &Self::Target {
2345        &self.inner
2346    }
2347}
2348
2349impl std::cmp::PartialEq for RegexWrapper {
2350    fn eq(&self, other: &Self) -> bool {
2351        self.as_str() == other.as_str()
2352    }
2353}
2354
2355impl Eq for RegexWrapper {}
2356
2357impl From<RegexWrapper> for regex::Regex {
2358    fn from(wrapper: RegexWrapper) -> Self {
2359        wrapper.inner
2360    }
2361}
2362
2363impl From<regex::Regex> for RegexWrapper {
2364    fn from(re: Regex) -> Self {
2365        Self { inner: re }
2366    }
2367}
2368
2369mod serde_regex {
2370    use regex::Regex;
2371    use serde::{Deserialize, Deserializer, Serializer};
2372
2373    pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2374    where
2375        S: Serializer,
2376    {
2377        serializer.serialize_str(value.as_str())
2378    }
2379
2380    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2381    where
2382        D: Deserializer<'de>,
2383    {
2384        let s = String::deserialize(deserializer)?;
2385        Regex::new(&s).map_err(serde::de::Error::custom)
2386    }
2387}
2388
2389/// Ser/de `globset::Glob` explicitly to handle `Option<Glob>` properly
2390pub(crate) mod from_opt_glob {
2391    use serde::{Deserialize, Deserializer, Serializer};
2392
2393    pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2394    where
2395        S: Serializer,
2396    {
2397        match value {
2398            Some(glob) => serializer.serialize_str(glob.glob()),
2399            None => serializer.serialize_none(),
2400        }
2401    }
2402
2403    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2404    where
2405        D: Deserializer<'de>,
2406    {
2407        let s: Option<String> = Option::deserialize(deserializer)?;
2408        if let Some(s) = s {
2409            return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2410        }
2411        Ok(None)
2412    }
2413}
2414
2415/// Parses a config profile
2416///
2417/// All `Profile` date is ignored by serde, however the `Config::to_string_pretty` includes it and
2418/// returns a toml table like
2419///
2420/// ```toml
2421/// #[profile.default]
2422/// src = "..."
2423/// ```
2424/// This ignores the `#[profile.default]` part in the toml
2425pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2426    s: &str,
2427) -> Result<Option<(Profile, T)>, Error> {
2428    let figment = Config::merge_toml_provider(
2429        Figment::new(),
2430        Toml::string(s).nested(),
2431        Config::DEFAULT_PROFILE,
2432    );
2433    if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2434        Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2435    } else {
2436        Ok(None)
2437    }
2438}
2439
2440impl Provider for Config {
2441    fn metadata(&self) -> Metadata {
2442        Metadata::named("Foundry Config")
2443    }
2444
2445    #[track_caller]
2446    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2447        let mut data = Serialized::defaults(self).data()?;
2448        if let Some(entry) = data.get_mut(&self.profile) {
2449            entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2450        }
2451        Ok(data)
2452    }
2453
2454    fn profile(&self) -> Option<Profile> {
2455        Some(self.profile.clone())
2456    }
2457}
2458
2459impl Default for Config {
2460    fn default() -> Self {
2461        Self {
2462            profile: Self::DEFAULT_PROFILE,
2463            profiles: vec![Self::DEFAULT_PROFILE],
2464            fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2465            isolate: cfg!(feature = "isolate-by-default"),
2466            root: root_default(),
2467            extends: None,
2468            src: "src".into(),
2469            test: "test".into(),
2470            script: "script".into(),
2471            out: "out".into(),
2472            libs: vec!["lib".into()],
2473            cache: true,
2474            dynamic_test_linking: false,
2475            cache_path: "cache".into(),
2476            broadcast: "broadcast".into(),
2477            snapshots: "snapshots".into(),
2478            gas_snapshot_check: false,
2479            gas_snapshot_emit: true,
2480            allow_paths: vec![],
2481            include_paths: vec![],
2482            force: false,
2483            evm_version: EvmVersion::Prague,
2484            gas_reports: vec!["*".to_string()],
2485            gas_reports_ignore: vec![],
2486            gas_reports_include_tests: false,
2487            solc: None,
2488            vyper: Default::default(),
2489            auto_detect_solc: true,
2490            offline: false,
2491            optimizer: None,
2492            optimizer_runs: None,
2493            optimizer_details: None,
2494            model_checker: None,
2495            extra_output: Default::default(),
2496            extra_output_files: Default::default(),
2497            names: false,
2498            sizes: false,
2499            test_pattern: None,
2500            test_pattern_inverse: None,
2501            contract_pattern: None,
2502            contract_pattern_inverse: None,
2503            path_pattern: None,
2504            path_pattern_inverse: None,
2505            coverage_pattern_inverse: None,
2506            test_failures_file: "cache/test-failures".into(),
2507            threads: None,
2508            show_progress: false,
2509            fuzz: FuzzConfig::new("cache/fuzz".into()),
2510            invariant: InvariantConfig::new("cache/invariant".into()),
2511            always_use_create_2_factory: false,
2512            ffi: false,
2513            allow_internal_expect_revert: false,
2514            prompt_timeout: 120,
2515            sender: Self::DEFAULT_SENDER,
2516            tx_origin: Self::DEFAULT_SENDER,
2517            initial_balance: U256::from((1u128 << 96) - 1),
2518            block_number: U256::from(1),
2519            fork_block_number: None,
2520            chain: None,
2521            gas_limit: (1u64 << 30).into(), // ~1B
2522            code_size_limit: None,
2523            gas_price: None,
2524            block_base_fee_per_gas: 0,
2525            block_coinbase: Address::ZERO,
2526            block_timestamp: U256::from(1),
2527            block_difficulty: 0,
2528            block_prevrandao: Default::default(),
2529            block_gas_limit: None,
2530            disable_block_gas_limit: false,
2531            enable_tx_gas_limit: false,
2532            memory_limit: 1 << 27, // 2**27 = 128MiB = 134_217_728 bytes
2533            eth_rpc_url: None,
2534            eth_rpc_accept_invalid_certs: false,
2535            eth_rpc_jwt: None,
2536            eth_rpc_timeout: None,
2537            eth_rpc_headers: None,
2538            etherscan_api_key: None,
2539            verbosity: 0,
2540            remappings: vec![],
2541            auto_detect_remappings: true,
2542            libraries: vec![],
2543            ignored_error_codes: vec![
2544                SolidityErrorCode::SpdxLicenseNotProvided,
2545                SolidityErrorCode::ContractExceeds24576Bytes,
2546                SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2547                SolidityErrorCode::TransientStorageUsed,
2548            ],
2549            ignored_file_paths: vec![],
2550            deny: DenyLevel::Never,
2551            deny_warnings: false,
2552            via_ir: false,
2553            ast: false,
2554            rpc_storage_caching: Default::default(),
2555            rpc_endpoints: Default::default(),
2556            etherscan: Default::default(),
2557            no_storage_caching: false,
2558            no_rpc_rate_limit: false,
2559            use_literal_content: false,
2560            bytecode_hash: BytecodeHash::Ipfs,
2561            cbor_metadata: true,
2562            revert_strings: None,
2563            sparse_mode: false,
2564            build_info: false,
2565            build_info_path: None,
2566            fmt: Default::default(),
2567            lint: Default::default(),
2568            doc: Default::default(),
2569            bind_json: Default::default(),
2570            labels: Default::default(),
2571            unchecked_cheatcode_artifacts: false,
2572            create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2573            create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2574            skip: vec![],
2575            dependencies: Default::default(),
2576            soldeer: Default::default(),
2577            assertions_revert: true,
2578            legacy_assertions: false,
2579            warnings: vec![],
2580            extra_args: vec![],
2581            networks: Default::default(),
2582            transaction_timeout: 120,
2583            additional_compiler_profiles: Default::default(),
2584            compilation_restrictions: Default::default(),
2585            script_execution_protection: true,
2586            _non_exhaustive: (),
2587        }
2588    }
2589}
2590
2591/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number
2592/// because integers are stored signed: <https://github.com/alexcrichton/toml-rs/issues/256>
2593///
2594/// Due to this limitation this type will be serialized/deserialized as String if it's larger than
2595/// `i64`
2596#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2597pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2598
2599impl From<u64> for GasLimit {
2600    fn from(gas: u64) -> Self {
2601        Self(gas)
2602    }
2603}
2604
2605impl From<GasLimit> for u64 {
2606    fn from(gas: GasLimit) -> Self {
2607        gas.0
2608    }
2609}
2610
2611impl Serialize for GasLimit {
2612    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2613    where
2614        S: Serializer,
2615    {
2616        if self.0 == u64::MAX {
2617            serializer.serialize_str("max")
2618        } else if self.0 > i64::MAX as u64 {
2619            serializer.serialize_str(&self.0.to_string())
2620        } else {
2621            serializer.serialize_u64(self.0)
2622        }
2623    }
2624}
2625
2626/// Variants for selecting the [`Solc`] instance
2627#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2628#[serde(untagged)]
2629pub enum SolcReq {
2630    /// Requires a specific solc version, that's either already installed (via `svm`) or will be
2631    /// auto installed (via `svm`)
2632    Version(Version),
2633    /// Path to an existing local solc installation
2634    Local(PathBuf),
2635}
2636
2637impl SolcReq {
2638    /// Tries to get the solc version from the `SolcReq`
2639    ///
2640    /// If the `SolcReq` is a `Version` it will return the version, if it's a path to a binary it
2641    /// will try to get the version from the binary.
2642    fn try_version(&self) -> Result<Version, SolcError> {
2643        match self {
2644            Self::Version(version) => Ok(version.clone()),
2645            Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2646        }
2647    }
2648}
2649
2650impl<T: AsRef<str>> From<T> for SolcReq {
2651    fn from(s: T) -> Self {
2652        let s = s.as_ref();
2653        if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2654    }
2655}
2656
2657/// A subset of the foundry `Config`
2658/// used to initialize a `foundry.toml` file
2659///
2660/// # Example
2661///
2662/// ```rust
2663/// use foundry_config::{BasicConfig, Config};
2664/// use serde::Deserialize;
2665///
2666/// let my_config = Config::figment().extract::<BasicConfig>();
2667/// ```
2668#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2669pub struct BasicConfig {
2670    /// the profile tag: `[profile.default]`
2671    #[serde(skip)]
2672    pub profile: Profile,
2673    /// path of the source contracts dir, like `src` or `contracts`
2674    pub src: PathBuf,
2675    /// path to where artifacts shut be written to
2676    pub out: PathBuf,
2677    /// all library folders to include, `lib`, `node_modules`
2678    pub libs: Vec<PathBuf>,
2679    /// `Remappings` to use for this repo
2680    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2681    pub remappings: Vec<RelativeRemapping>,
2682}
2683
2684impl BasicConfig {
2685    /// Serialize the config as a String of TOML.
2686    ///
2687    /// This serializes to a table with the name of the profile
2688    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2689        let s = toml::to_string_pretty(self)?;
2690        Ok(format!(
2691            "\
2692[profile.{}]
2693{s}
2694# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2695            self.profile
2696        ))
2697    }
2698}
2699
2700pub(crate) mod from_str_lowercase {
2701    use serde::{Deserialize, Deserializer, Serializer};
2702    use std::str::FromStr;
2703
2704    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2705    where
2706        T: std::fmt::Display,
2707        S: Serializer,
2708    {
2709        serializer.collect_str(&value.to_string().to_lowercase())
2710    }
2711
2712    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2713    where
2714        D: Deserializer<'de>,
2715        T: FromStr,
2716        T::Err: std::fmt::Display,
2717    {
2718        String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2719    }
2720}
2721
2722fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2723    let path = path.into();
2724    foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2725}
2726
2727fn root_default() -> PathBuf {
2728    ".".into()
2729}
2730
2731#[cfg(test)]
2732mod tests {
2733    use super::*;
2734    use crate::{
2735        cache::{CachedChains, CachedEndpoints},
2736        endpoints::RpcEndpointType,
2737        etherscan::ResolvedEtherscanConfigs,
2738        fmt::IndentStyle,
2739    };
2740    use NamedChain::Moonbeam;
2741    use endpoints::{RpcAuth, RpcEndpointConfig};
2742    use figment::error::Kind::InvalidType;
2743    use foundry_compilers::artifacts::{
2744        ModelCheckerEngine, YulDetails, vyper::VyperOptimizationMode,
2745    };
2746    use similar_asserts::assert_eq;
2747    use soldeer_core::remappings::RemappingsLocation;
2748    use std::{fs::File, io::Write};
2749    use tempfile::tempdir;
2750
2751    // Helper function to clear `__warnings` in config, since it will be populated during loading
2752    // from file, causing testing problem when comparing to those created from `default()`, etc.
2753    fn clear_warning(config: &mut Config) {
2754        config.warnings = vec![];
2755    }
2756
2757    #[test]
2758    fn default_sender() {
2759        assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2760    }
2761
2762    #[test]
2763    fn test_caching() {
2764        let mut config = Config::default();
2765        let chain_id = NamedChain::Mainnet;
2766        let url = "https://eth-mainnet.alchemyapi";
2767        assert!(config.enable_caching(url, chain_id));
2768
2769        config.no_storage_caching = true;
2770        assert!(!config.enable_caching(url, chain_id));
2771
2772        config.no_storage_caching = false;
2773        assert!(!config.enable_caching(url, NamedChain::Dev));
2774    }
2775
2776    #[test]
2777    fn test_install_dir() {
2778        figment::Jail::expect_with(|jail| {
2779            let config = Config::load().unwrap();
2780            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2781            jail.create_file(
2782                "foundry.toml",
2783                r"
2784                [profile.default]
2785                libs = ['node_modules', 'lib']
2786            ",
2787            )?;
2788            let config = Config::load().unwrap();
2789            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2790
2791            jail.create_file(
2792                "foundry.toml",
2793                r"
2794                [profile.default]
2795                libs = ['custom', 'node_modules', 'lib']
2796            ",
2797            )?;
2798            let config = Config::load().unwrap();
2799            assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2800
2801            Ok(())
2802        });
2803    }
2804
2805    #[test]
2806    fn test_figment_is_default() {
2807        figment::Jail::expect_with(|_| {
2808            let mut default: Config = Config::figment().extract()?;
2809            let default2 = Config::default();
2810            default.profile = default2.profile.clone();
2811            default.profiles = default2.profiles.clone();
2812            assert_eq!(default, default2);
2813            Ok(())
2814        });
2815    }
2816
2817    #[test]
2818    fn figment_profiles() {
2819        figment::Jail::expect_with(|jail| {
2820            jail.create_file(
2821                "foundry.toml",
2822                r"
2823                [foo.baz]
2824                libs = ['node_modules', 'lib']
2825
2826                [profile.default]
2827                libs = ['node_modules', 'lib']
2828
2829                [profile.ci]
2830                libs = ['node_modules', 'lib']
2831
2832                [profile.local]
2833                libs = ['node_modules', 'lib']
2834            ",
2835            )?;
2836
2837            let config = crate::Config::load().unwrap();
2838            let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
2839            assert_eq!(config.profiles, expected);
2840
2841            Ok(())
2842        });
2843    }
2844
2845    #[test]
2846    fn test_default_round_trip() {
2847        figment::Jail::expect_with(|_| {
2848            let original = Config::figment();
2849            let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
2850            for figment in &[original, roundtrip] {
2851                let config = Config::from_provider(figment).unwrap();
2852                assert_eq!(config, Config::default().normalized_optimizer_settings());
2853            }
2854            Ok(())
2855        });
2856    }
2857
2858    #[test]
2859    fn ffi_env_disallowed() {
2860        figment::Jail::expect_with(|jail| {
2861            jail.set_env("FOUNDRY_FFI", "true");
2862            jail.set_env("FFI", "true");
2863            jail.set_env("DAPP_FFI", "true");
2864            let config = Config::load().unwrap();
2865            assert!(!config.ffi);
2866
2867            Ok(())
2868        });
2869    }
2870
2871    #[test]
2872    fn test_profile_env() {
2873        figment::Jail::expect_with(|jail| {
2874            jail.set_env("FOUNDRY_PROFILE", "default");
2875            let figment = Config::figment();
2876            assert_eq!(figment.profile(), "default");
2877
2878            jail.set_env("FOUNDRY_PROFILE", "hardhat");
2879            let figment: Figment = Config::hardhat().into();
2880            assert_eq!(figment.profile(), "hardhat");
2881
2882            jail.create_file(
2883                "foundry.toml",
2884                r"
2885                [profile.default]
2886                libs = ['lib']
2887                [profile.local]
2888                libs = ['modules']
2889            ",
2890            )?;
2891            jail.set_env("FOUNDRY_PROFILE", "local");
2892            let config = Config::load().unwrap();
2893            assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2894
2895            Ok(())
2896        });
2897    }
2898
2899    #[test]
2900    fn test_default_test_path() {
2901        figment::Jail::expect_with(|_| {
2902            let config = Config::default();
2903            let paths_config = config.project_paths::<Solc>();
2904            assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2905            Ok(())
2906        });
2907    }
2908
2909    #[test]
2910    fn test_default_libs() {
2911        figment::Jail::expect_with(|jail| {
2912            let config = Config::load().unwrap();
2913            assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2914
2915            fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2916            let config = Config::load().unwrap();
2917            assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2918
2919            fs::create_dir_all(jail.directory().join("lib")).unwrap();
2920            let config = Config::load().unwrap();
2921            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2922
2923            Ok(())
2924        });
2925    }
2926
2927    #[test]
2928    fn test_inheritance_from_default_test_path() {
2929        figment::Jail::expect_with(|jail| {
2930            jail.create_file(
2931                "foundry.toml",
2932                r#"
2933                [profile.default]
2934                test = "defaulttest"
2935                src  = "defaultsrc"
2936                libs = ['lib', 'node_modules']
2937
2938                [profile.custom]
2939                src = "customsrc"
2940            "#,
2941            )?;
2942
2943            let config = Config::load().unwrap();
2944            assert_eq!(config.src, PathBuf::from("defaultsrc"));
2945            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2946
2947            jail.set_env("FOUNDRY_PROFILE", "custom");
2948            let config = Config::load().unwrap();
2949            assert_eq!(config.src, PathBuf::from("customsrc"));
2950            assert_eq!(config.test, PathBuf::from("defaulttest"));
2951            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2952
2953            Ok(())
2954        });
2955    }
2956
2957    #[test]
2958    fn test_custom_test_path() {
2959        figment::Jail::expect_with(|jail| {
2960            jail.create_file(
2961                "foundry.toml",
2962                r#"
2963                [profile.default]
2964                test = "mytest"
2965            "#,
2966            )?;
2967
2968            let config = Config::load().unwrap();
2969            let paths_config = config.project_paths::<Solc>();
2970            assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
2971            Ok(())
2972        });
2973    }
2974
2975    #[test]
2976    fn test_remappings() {
2977        figment::Jail::expect_with(|jail| {
2978            jail.create_file(
2979                "foundry.toml",
2980                r#"
2981                [profile.default]
2982                src = "some-source"
2983                out = "some-out"
2984                cache = true
2985            "#,
2986            )?;
2987            let config = Config::load().unwrap();
2988            assert!(config.remappings.is_empty());
2989
2990            jail.create_file(
2991                "remappings.txt",
2992                r"
2993                file-ds-test/=lib/ds-test/
2994                file-other/=lib/other/
2995            ",
2996            )?;
2997
2998            let config = Config::load().unwrap();
2999            assert_eq!(
3000                config.remappings,
3001                vec![
3002                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3003                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3004                ],
3005            );
3006
3007            jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3008            let config = Config::load().unwrap();
3009
3010            assert_eq!(
3011                config.remappings,
3012                vec![
3013                    // From environment (should have precedence over remapping.txt)
3014                    Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3015                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3016                    // From remapping.txt (should have less precedence than remapping.txt)
3017                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3018                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3019                ],
3020            );
3021
3022            Ok(())
3023        });
3024    }
3025
3026    #[test]
3027    fn test_remappings_override() {
3028        figment::Jail::expect_with(|jail| {
3029            jail.create_file(
3030                "foundry.toml",
3031                r#"
3032                [profile.default]
3033                src = "some-source"
3034                out = "some-out"
3035                cache = true
3036            "#,
3037            )?;
3038            let config = Config::load().unwrap();
3039            assert!(config.remappings.is_empty());
3040
3041            jail.create_file(
3042                "remappings.txt",
3043                r"
3044                ds-test/=lib/ds-test/
3045                other/=lib/other/
3046            ",
3047            )?;
3048
3049            let config = Config::load().unwrap();
3050            assert_eq!(
3051                config.remappings,
3052                vec![
3053                    Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3054                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3055                ],
3056            );
3057
3058            jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3059            let config = Config::load().unwrap();
3060
3061            // Remappings should now be:
3062            // - ds-test from environment (lib/ds-test/src/)
3063            // - other from remappings.txt (lib/other/)
3064            // - env-lib from environment (lib/env-lib/)
3065            assert_eq!(
3066                config.remappings,
3067                vec![
3068                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3069                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3070                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3071                ],
3072            );
3073
3074            // contains additional remapping to the source dir
3075            assert_eq!(
3076                config.get_all_remappings().collect::<Vec<_>>(),
3077                vec![
3078                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3079                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3080                    Remapping::from_str("other/=lib/other/").unwrap(),
3081                ],
3082            );
3083
3084            Ok(())
3085        });
3086    }
3087
3088    #[test]
3089    fn test_can_update_libs() {
3090        figment::Jail::expect_with(|jail| {
3091            jail.create_file(
3092                "foundry.toml",
3093                r#"
3094                [profile.default]
3095                libs = ["node_modules"]
3096            "#,
3097            )?;
3098
3099            let mut config = Config::load().unwrap();
3100            config.libs.push("libs".into());
3101            config.update_libs().unwrap();
3102
3103            let config = Config::load().unwrap();
3104            assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3105            Ok(())
3106        });
3107    }
3108
3109    #[test]
3110    fn test_large_gas_limit() {
3111        figment::Jail::expect_with(|jail| {
3112            let gas = u64::MAX;
3113            jail.create_file(
3114                "foundry.toml",
3115                &format!(
3116                    r#"
3117                [profile.default]
3118                gas_limit = "{gas}"
3119            "#
3120                ),
3121            )?;
3122
3123            let config = Config::load().unwrap();
3124            assert_eq!(
3125                config,
3126                Config {
3127                    gas_limit: gas.into(),
3128                    ..Config::default().normalized_optimizer_settings()
3129                }
3130            );
3131
3132            Ok(())
3133        });
3134    }
3135
3136    #[test]
3137    #[should_panic]
3138    fn test_toml_file_parse_failure() {
3139        figment::Jail::expect_with(|jail| {
3140            jail.create_file(
3141                "foundry.toml",
3142                r#"
3143                [profile.default]
3144                eth_rpc_url = "https://example.com/
3145            "#,
3146            )?;
3147
3148            let _config = Config::load().unwrap();
3149
3150            Ok(())
3151        });
3152    }
3153
3154    #[test]
3155    #[should_panic]
3156    fn test_toml_file_non_existing_config_var_failure() {
3157        figment::Jail::expect_with(|jail| {
3158            jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3159
3160            let _config = Config::load().unwrap();
3161
3162            Ok(())
3163        });
3164    }
3165
3166    #[test]
3167    fn test_resolve_etherscan_with_chain() {
3168        figment::Jail::expect_with(|jail| {
3169            let env_key = "__BSC_ETHERSCAN_API_KEY";
3170            let env_value = "env value";
3171            jail.create_file(
3172                "foundry.toml",
3173                r#"
3174                [profile.default]
3175
3176                [etherscan]
3177                bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3178            "#,
3179            )?;
3180
3181            let config = Config::load().unwrap();
3182            assert!(
3183                config
3184                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3185                    .is_err()
3186            );
3187
3188            unsafe {
3189                std::env::set_var(env_key, env_value);
3190            }
3191
3192            assert_eq!(
3193                config
3194                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3195                    .unwrap()
3196                    .unwrap()
3197                    .key,
3198                env_value
3199            );
3200
3201            let mut with_key = config;
3202            with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3203
3204            assert_eq!(
3205                with_key
3206                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3207                    .unwrap()
3208                    .unwrap()
3209                    .key,
3210                "via etherscan_api_key"
3211            );
3212
3213            unsafe {
3214                std::env::remove_var(env_key);
3215            }
3216            Ok(())
3217        });
3218    }
3219
3220    #[test]
3221    fn test_resolve_etherscan() {
3222        figment::Jail::expect_with(|jail| {
3223            jail.create_file(
3224                "foundry.toml",
3225                r#"
3226                [profile.default]
3227
3228                [etherscan]
3229                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3230                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3231            "#,
3232            )?;
3233
3234            let config = Config::load().unwrap();
3235
3236            assert!(config.etherscan.clone().resolved().has_unresolved());
3237
3238            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3239
3240            let configs = config.etherscan.resolved();
3241            assert!(!configs.has_unresolved());
3242
3243            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3244            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3245            assert_eq!(
3246                configs,
3247                ResolvedEtherscanConfigs::new([
3248                    (
3249                        "mainnet",
3250                        ResolvedEtherscanConfig {
3251                            api_url: mainnet_urls.0.to_string(),
3252                            chain: Some(NamedChain::Mainnet.into()),
3253                            browser_url: Some(mainnet_urls.1.to_string()),
3254                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3255                        }
3256                    ),
3257                    (
3258                        "moonbeam",
3259                        ResolvedEtherscanConfig {
3260                            api_url: mb_urls.0.to_string(),
3261                            chain: Some(Moonbeam.into()),
3262                            browser_url: Some(mb_urls.1.to_string()),
3263                            key: "123456789".to_string(),
3264                        }
3265                    ),
3266                ])
3267            );
3268
3269            Ok(())
3270        });
3271    }
3272
3273    #[test]
3274    fn test_resolve_etherscan_with_versions() {
3275        figment::Jail::expect_with(|jail| {
3276            jail.create_file(
3277                "foundry.toml",
3278                r#"
3279                [profile.default]
3280
3281                [etherscan]
3282                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3283                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3284            "#,
3285            )?;
3286
3287            let config = Config::load().unwrap();
3288
3289            assert!(config.etherscan.clone().resolved().has_unresolved());
3290
3291            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3292
3293            let configs = config.etherscan.resolved();
3294            assert!(!configs.has_unresolved());
3295
3296            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3297            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3298            assert_eq!(
3299                configs,
3300                ResolvedEtherscanConfigs::new([
3301                    (
3302                        "mainnet",
3303                        ResolvedEtherscanConfig {
3304                            api_url: mainnet_urls.0.to_string(),
3305                            chain: Some(NamedChain::Mainnet.into()),
3306                            browser_url: Some(mainnet_urls.1.to_string()),
3307                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3308                        }
3309                    ),
3310                    (
3311                        "moonbeam",
3312                        ResolvedEtherscanConfig {
3313                            api_url: mb_urls.0.to_string(),
3314                            chain: Some(Moonbeam.into()),
3315                            browser_url: Some(mb_urls.1.to_string()),
3316                            key: "123456789".to_string(),
3317                        }
3318                    ),
3319                ])
3320            );
3321
3322            Ok(())
3323        });
3324    }
3325
3326    #[test]
3327    fn test_resolve_etherscan_chain_id() {
3328        figment::Jail::expect_with(|jail| {
3329            jail.create_file(
3330                "foundry.toml",
3331                r#"
3332                [profile.default]
3333                chain_id = "sepolia"
3334
3335                [etherscan]
3336                sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3337            "#,
3338            )?;
3339
3340            let config = Config::load().unwrap();
3341            let etherscan = config.get_etherscan_config().unwrap().unwrap();
3342            assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3343            assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3344
3345            Ok(())
3346        });
3347    }
3348
3349    #[test]
3350    fn test_resolve_rpc_url() {
3351        figment::Jail::expect_with(|jail| {
3352            jail.create_file(
3353                "foundry.toml",
3354                r#"
3355                [profile.default]
3356                [rpc_endpoints]
3357                optimism = "https://example.com/"
3358                mainnet = "${_CONFIG_MAINNET}"
3359            "#,
3360            )?;
3361            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3362
3363            let mut config = Config::load().unwrap();
3364            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3365
3366            config.eth_rpc_url = Some("mainnet".to_string());
3367            assert_eq!(
3368                "https://eth-mainnet.alchemyapi.io/v2/123455",
3369                config.get_rpc_url_or_localhost_http().unwrap()
3370            );
3371
3372            config.eth_rpc_url = Some("optimism".to_string());
3373            assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3374
3375            Ok(())
3376        })
3377    }
3378
3379    #[test]
3380    fn test_resolve_rpc_url_if_etherscan_set() {
3381        figment::Jail::expect_with(|jail| {
3382            jail.create_file(
3383                "foundry.toml",
3384                r#"
3385                [profile.default]
3386                etherscan_api_key = "dummy"
3387                [rpc_endpoints]
3388                optimism = "https://example.com/"
3389            "#,
3390            )?;
3391
3392            let config = Config::load().unwrap();
3393            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3394
3395            Ok(())
3396        })
3397    }
3398
3399    #[test]
3400    fn test_resolve_rpc_url_alias() {
3401        figment::Jail::expect_with(|jail| {
3402            jail.create_file(
3403                "foundry.toml",
3404                r#"
3405                [profile.default]
3406                [rpc_endpoints]
3407                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3408            "#,
3409            )?;
3410            let mut config = Config::load().unwrap();
3411            config.eth_rpc_url = Some("polygonAmoy".to_string());
3412            assert!(config.get_rpc_url().unwrap().is_err());
3413
3414            jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3415
3416            let mut config = Config::load().unwrap();
3417            config.eth_rpc_url = Some("polygonAmoy".to_string());
3418            assert_eq!(
3419                "https://polygon-amoy.g.alchemy.com/v2/123455",
3420                config.get_rpc_url().unwrap().unwrap()
3421            );
3422
3423            Ok(())
3424        })
3425    }
3426
3427    #[test]
3428    fn test_resolve_rpc_aliases() {
3429        figment::Jail::expect_with(|jail| {
3430            jail.create_file(
3431                "foundry.toml",
3432                r#"
3433               [profile.default]
3434               [etherscan]
3435               arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3436               [rpc_endpoints]
3437               arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3438            "#,
3439            )?;
3440
3441            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3442            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3443
3444            let config = Config::load().unwrap();
3445
3446            let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3447            assert!(config.is_err());
3448            assert_eq!(
3449                config.unwrap_err().to_string(),
3450                "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3451            );
3452
3453            Ok(())
3454        });
3455    }
3456
3457    #[test]
3458    fn test_resolve_rpc_config() {
3459        figment::Jail::expect_with(|jail| {
3460            jail.create_file(
3461                "foundry.toml",
3462                r#"
3463                [rpc_endpoints]
3464                optimism = "https://example.com/"
3465                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3466            "#,
3467            )?;
3468            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3469
3470            let config = Config::load().unwrap();
3471            assert_eq!(
3472                RpcEndpoints::new([
3473                    (
3474                        "optimism",
3475                        RpcEndpointType::String(RpcEndpointUrl::Url(
3476                            "https://example.com/".to_string()
3477                        ))
3478                    ),
3479                    (
3480                        "mainnet",
3481                        RpcEndpointType::Config(RpcEndpoint {
3482                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3483                            config: RpcEndpointConfig {
3484                                retries: Some(3),
3485                                retry_backoff: Some(1000),
3486                                compute_units_per_second: Some(1000),
3487                            },
3488                            auth: None,
3489                        })
3490                    ),
3491                ]),
3492                config.rpc_endpoints
3493            );
3494
3495            let resolved = config.rpc_endpoints.resolved();
3496            assert_eq!(
3497                RpcEndpoints::new([
3498                    (
3499                        "optimism",
3500                        RpcEndpointType::String(RpcEndpointUrl::Url(
3501                            "https://example.com/".to_string()
3502                        ))
3503                    ),
3504                    (
3505                        "mainnet",
3506                        RpcEndpointType::Config(RpcEndpoint {
3507                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3508                            config: RpcEndpointConfig {
3509                                retries: Some(3),
3510                                retry_backoff: Some(1000),
3511                                compute_units_per_second: Some(1000),
3512                            },
3513                            auth: None,
3514                        })
3515                    ),
3516                ])
3517                .resolved(),
3518                resolved
3519            );
3520            Ok(())
3521        })
3522    }
3523
3524    #[test]
3525    fn test_resolve_auth() {
3526        figment::Jail::expect_with(|jail| {
3527            jail.create_file(
3528                "foundry.toml",
3529                r#"
3530                [profile.default]
3531                eth_rpc_url = "optimism"
3532                [rpc_endpoints]
3533                optimism = "https://example.com/"
3534                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3535            "#,
3536            )?;
3537
3538            let config = Config::load().unwrap();
3539
3540            jail.set_env("_CONFIG_AUTH", "123456");
3541            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3542
3543            assert_eq!(
3544                RpcEndpoints::new([
3545                    (
3546                        "optimism",
3547                        RpcEndpointType::String(RpcEndpointUrl::Url(
3548                            "https://example.com/".to_string()
3549                        ))
3550                    ),
3551                    (
3552                        "mainnet",
3553                        RpcEndpointType::Config(RpcEndpoint {
3554                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3555                            config: RpcEndpointConfig {
3556                                retries: Some(3),
3557                                retry_backoff: Some(1000),
3558                                compute_units_per_second: Some(1000)
3559                            },
3560                            auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3561                        })
3562                    ),
3563                ]),
3564                config.rpc_endpoints
3565            );
3566            let resolved = config.rpc_endpoints.resolved();
3567            assert_eq!(
3568                RpcEndpoints::new([
3569                    (
3570                        "optimism",
3571                        RpcEndpointType::String(RpcEndpointUrl::Url(
3572                            "https://example.com/".to_string()
3573                        ))
3574                    ),
3575                    (
3576                        "mainnet",
3577                        RpcEndpointType::Config(RpcEndpoint {
3578                            endpoint: RpcEndpointUrl::Url(
3579                                "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3580                            ),
3581                            config: RpcEndpointConfig {
3582                                retries: Some(3),
3583                                retry_backoff: Some(1000),
3584                                compute_units_per_second: Some(1000)
3585                            },
3586                            auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3587                        })
3588                    ),
3589                ])
3590                .resolved(),
3591                resolved
3592            );
3593
3594            Ok(())
3595        });
3596    }
3597
3598    #[test]
3599    fn test_resolve_endpoints() {
3600        figment::Jail::expect_with(|jail| {
3601            jail.create_file(
3602                "foundry.toml",
3603                r#"
3604                [profile.default]
3605                eth_rpc_url = "optimism"
3606                [rpc_endpoints]
3607                optimism = "https://example.com/"
3608                mainnet = "${_CONFIG_MAINNET}"
3609                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3610                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3611            "#,
3612            )?;
3613
3614            let config = Config::load().unwrap();
3615
3616            assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3617
3618            assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3619
3620            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3621            jail.set_env("_CONFIG_API_KEY1", "123456");
3622            jail.set_env("_CONFIG_API_KEY2", "98765");
3623
3624            let endpoints = config.rpc_endpoints.resolved();
3625
3626            assert!(!endpoints.has_unresolved());
3627
3628            assert_eq!(
3629                endpoints,
3630                RpcEndpoints::new([
3631                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3632                    (
3633                        "mainnet",
3634                        RpcEndpointUrl::Url(
3635                            "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3636                        )
3637                    ),
3638                    (
3639                        "mainnet_2",
3640                        RpcEndpointUrl::Url(
3641                            "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3642                        )
3643                    ),
3644                    (
3645                        "mainnet_3",
3646                        RpcEndpointUrl::Url(
3647                            "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3648                        )
3649                    ),
3650                ])
3651                .resolved()
3652            );
3653
3654            Ok(())
3655        });
3656    }
3657
3658    #[test]
3659    fn test_extract_etherscan_config() {
3660        figment::Jail::expect_with(|jail| {
3661            jail.create_file(
3662                "foundry.toml",
3663                r#"
3664                [profile.default]
3665                etherscan_api_key = "optimism"
3666
3667                [etherscan]
3668                optimism = { key = "https://etherscan-optimism.com/" }
3669                amoy = { key = "https://etherscan-amoy.com/" }
3670            "#,
3671            )?;
3672
3673            let mut config = Config::load().unwrap();
3674
3675            let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3676            assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3677
3678            config.etherscan_api_key = Some("amoy".to_string());
3679
3680            let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
3681            assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
3682
3683            Ok(())
3684        });
3685    }
3686
3687    #[test]
3688    fn test_extract_etherscan_config_by_chain() {
3689        figment::Jail::expect_with(|jail| {
3690            jail.create_file(
3691                "foundry.toml",
3692                r#"
3693                [profile.default]
3694
3695                [etherscan]
3696                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
3697            "#,
3698            )?;
3699
3700            let config = Config::load().unwrap();
3701
3702            let amoy = config
3703                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3704                .unwrap()
3705                .unwrap();
3706            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3707
3708            Ok(())
3709        });
3710    }
3711
3712    #[test]
3713    fn test_extract_etherscan_config_by_chain_with_url() {
3714        figment::Jail::expect_with(|jail| {
3715            jail.create_file(
3716                "foundry.toml",
3717                r#"
3718                [profile.default]
3719
3720                [etherscan]
3721                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url =  "https://verifier-url.com/"}
3722            "#,
3723            )?;
3724
3725            let config = Config::load().unwrap();
3726
3727            let amoy = config
3728                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3729                .unwrap()
3730                .unwrap();
3731            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3732            assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
3733
3734            Ok(())
3735        });
3736    }
3737
3738    #[test]
3739    fn test_extract_etherscan_config_by_chain_and_alias() {
3740        figment::Jail::expect_with(|jail| {
3741            jail.create_file(
3742                "foundry.toml",
3743                r#"
3744                [profile.default]
3745                eth_rpc_url = "amoy"
3746
3747                [etherscan]
3748                amoy = { key = "https://etherscan-amoy.com/" }
3749
3750                [rpc_endpoints]
3751                amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
3752            "#,
3753            )?;
3754
3755            let config = Config::load().unwrap();
3756
3757            let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3758            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3759
3760            let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
3761            assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
3762            Ok(())
3763        });
3764    }
3765
3766    #[test]
3767    fn test_toml_file() {
3768        figment::Jail::expect_with(|jail| {
3769            jail.create_file(
3770                "foundry.toml",
3771                r#"
3772                [profile.default]
3773                src = "some-source"
3774                out = "some-out"
3775                cache = true
3776                eth_rpc_url = "https://example.com/"
3777                verbosity = 3
3778                remappings = ["ds-test=lib/ds-test/"]
3779                via_ir = true
3780                rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3781                use_literal_content = false
3782                bytecode_hash = "ipfs"
3783                cbor_metadata = true
3784                revert_strings = "strip"
3785                allow_paths = ["allow", "paths"]
3786                build_info_path = "build-info"
3787                always_use_create_2_factory = true
3788
3789                [rpc_endpoints]
3790                optimism = "https://example.com/"
3791                mainnet = "${RPC_MAINNET}"
3792                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3793                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3794            "#,
3795            )?;
3796
3797            let config = Config::load().unwrap();
3798            assert_eq!(
3799                config,
3800                Config {
3801                    src: "some-source".into(),
3802                    out: "some-out".into(),
3803                    cache: true,
3804                    eth_rpc_url: Some("https://example.com/".to_string()),
3805                    remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3806                    verbosity: 3,
3807                    via_ir: true,
3808                    rpc_storage_caching: StorageCachingConfig {
3809                        chains: CachedChains::Chains(vec![
3810                            Chain::mainnet(),
3811                            Chain::optimism_mainnet(),
3812                            Chain::from_id(999999)
3813                        ]),
3814                        endpoints: CachedEndpoints::All,
3815                    },
3816                    use_literal_content: false,
3817                    bytecode_hash: BytecodeHash::Ipfs,
3818                    cbor_metadata: true,
3819                    revert_strings: Some(RevertStrings::Strip),
3820                    allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3821                    rpc_endpoints: RpcEndpoints::new([
3822                        ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3823                        ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3824                        (
3825                            "mainnet_2",
3826                            RpcEndpointUrl::Env(
3827                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3828                            )
3829                        ),
3830                        (
3831                            "mainnet_3",
3832                            RpcEndpointUrl::Env(
3833                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3834                                    .to_string()
3835                            )
3836                        ),
3837                    ]),
3838                    build_info_path: Some("build-info".into()),
3839                    always_use_create_2_factory: true,
3840                    ..Config::default().normalized_optimizer_settings()
3841                }
3842            );
3843
3844            Ok(())
3845        });
3846    }
3847
3848    #[test]
3849    fn test_load_remappings() {
3850        figment::Jail::expect_with(|jail| {
3851            jail.create_file(
3852                "foundry.toml",
3853                r"
3854                [profile.default]
3855                remappings = ['nested/=lib/nested/']
3856            ",
3857            )?;
3858
3859            let config = Config::load_with_root(jail.directory()).unwrap();
3860            assert_eq!(
3861                config.remappings,
3862                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3863            );
3864
3865            Ok(())
3866        });
3867    }
3868
3869    #[test]
3870    fn test_load_full_toml() {
3871        figment::Jail::expect_with(|jail| {
3872            jail.create_file(
3873                "foundry.toml",
3874                r#"
3875                [profile.default]
3876                auto_detect_solc = true
3877                block_base_fee_per_gas = 0
3878                block_coinbase = '0x0000000000000000000000000000000000000000'
3879                block_difficulty = 0
3880                block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3881                block_number = 1
3882                block_timestamp = 1
3883                use_literal_content = false
3884                bytecode_hash = 'ipfs'
3885                cbor_metadata = true
3886                cache = true
3887                cache_path = 'cache'
3888                evm_version = 'london'
3889                extra_output = []
3890                extra_output_files = []
3891                always_use_create_2_factory = false
3892                ffi = false
3893                force = false
3894                gas_limit = 9223372036854775807
3895                gas_price = 0
3896                gas_reports = ['*']
3897                ignored_error_codes = [1878]
3898                ignored_warnings_from = ["something"]
3899                deny = "never"
3900                initial_balance = '0xffffffffffffffffffffffff'
3901                libraries = []
3902                libs = ['lib']
3903                memory_limit = 134217728
3904                names = false
3905                no_storage_caching = false
3906                no_rpc_rate_limit = false
3907                offline = false
3908                optimizer = true
3909                optimizer_runs = 200
3910                out = 'out'
3911                remappings = ['nested/=lib/nested/']
3912                sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3913                sizes = false
3914                sparse_mode = false
3915                src = 'src'
3916                test = 'test'
3917                tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3918                verbosity = 0
3919                via_ir = false
3920
3921                [profile.default.rpc_storage_caching]
3922                chains = 'all'
3923                endpoints = 'all'
3924
3925                [rpc_endpoints]
3926                optimism = "https://example.com/"
3927                mainnet = "${RPC_MAINNET}"
3928                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3929
3930                [fuzz]
3931                runs = 256
3932                seed = '0x3e8'
3933                max_test_rejects = 65536
3934
3935                [invariant]
3936                runs = 256
3937                depth = 500
3938                fail_on_revert = false
3939                call_override = false
3940                shrink_run_limit = 5000
3941            "#,
3942            )?;
3943
3944            let config = Config::load_with_root(jail.directory()).unwrap();
3945
3946            assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3947            assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3948            assert_eq!(
3949                config.remappings,
3950                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3951            );
3952
3953            assert_eq!(
3954                config.rpc_endpoints,
3955                RpcEndpoints::new([
3956                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3957                    ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3958                    (
3959                        "mainnet_2",
3960                        RpcEndpointUrl::Env(
3961                            "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3962                        )
3963                    ),
3964                ]),
3965            );
3966
3967            Ok(())
3968        });
3969    }
3970
3971    #[test]
3972    fn test_solc_req() {
3973        figment::Jail::expect_with(|jail| {
3974            jail.create_file(
3975                "foundry.toml",
3976                r#"
3977                [profile.default]
3978                solc_version = "0.8.12"
3979            "#,
3980            )?;
3981
3982            let config = Config::load().unwrap();
3983            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3984
3985            jail.create_file(
3986                "foundry.toml",
3987                r#"
3988                [profile.default]
3989                solc = "0.8.12"
3990            "#,
3991            )?;
3992
3993            let config = Config::load().unwrap();
3994            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3995
3996            jail.create_file(
3997                "foundry.toml",
3998                r#"
3999                [profile.default]
4000                solc = "path/to/local/solc"
4001            "#,
4002            )?;
4003
4004            let config = Config::load().unwrap();
4005            assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
4006
4007            jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
4008            let config = Config::load().unwrap();
4009            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
4010            Ok(())
4011        });
4012    }
4013
4014    // ensures the newer `solc` takes precedence over `solc_version`
4015    #[test]
4016    fn test_backwards_solc_version() {
4017        figment::Jail::expect_with(|jail| {
4018            jail.create_file(
4019                "foundry.toml",
4020                r#"
4021                [default]
4022                solc = "0.8.12"
4023                solc_version = "0.8.20"
4024            "#,
4025            )?;
4026
4027            let config = Config::load().unwrap();
4028            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4029
4030            Ok(())
4031        });
4032
4033        figment::Jail::expect_with(|jail| {
4034            jail.create_file(
4035                "foundry.toml",
4036                r#"
4037                [default]
4038                solc_version = "0.8.20"
4039            "#,
4040            )?;
4041
4042            let config = Config::load().unwrap();
4043            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
4044
4045            Ok(())
4046        });
4047    }
4048
4049    #[test]
4050    fn test_toml_casing_file() {
4051        figment::Jail::expect_with(|jail| {
4052            jail.create_file(
4053                "foundry.toml",
4054                r#"
4055                [profile.default]
4056                src = "some-source"
4057                out = "some-out"
4058                cache = true
4059                eth-rpc-url = "https://example.com/"
4060                evm-version = "berlin"
4061                auto-detect-solc = false
4062            "#,
4063            )?;
4064
4065            let config = Config::load().unwrap();
4066            assert_eq!(
4067                config,
4068                Config {
4069                    src: "some-source".into(),
4070                    out: "some-out".into(),
4071                    cache: true,
4072                    eth_rpc_url: Some("https://example.com/".to_string()),
4073                    auto_detect_solc: false,
4074                    evm_version: EvmVersion::Berlin,
4075                    ..Config::default().normalized_optimizer_settings()
4076                }
4077            );
4078
4079            Ok(())
4080        });
4081    }
4082
4083    #[test]
4084    fn test_output_selection() {
4085        figment::Jail::expect_with(|jail| {
4086            jail.create_file(
4087                "foundry.toml",
4088                r#"
4089                [profile.default]
4090                extra_output = ["metadata", "ir-optimized"]
4091                extra_output_files = ["metadata"]
4092            "#,
4093            )?;
4094
4095            let config = Config::load().unwrap();
4096
4097            assert_eq!(
4098                config.extra_output,
4099                vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4100            );
4101            assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4102
4103            Ok(())
4104        });
4105    }
4106
4107    #[test]
4108    fn test_precedence() {
4109        figment::Jail::expect_with(|jail| {
4110            jail.create_file(
4111                "foundry.toml",
4112                r#"
4113                [profile.default]
4114                src = "mysrc"
4115                out = "myout"
4116                verbosity = 3
4117            "#,
4118            )?;
4119
4120            let config = Config::load().unwrap();
4121            assert_eq!(
4122                config,
4123                Config {
4124                    src: "mysrc".into(),
4125                    out: "myout".into(),
4126                    verbosity: 3,
4127                    ..Config::default().normalized_optimizer_settings()
4128                }
4129            );
4130
4131            jail.set_env("FOUNDRY_SRC", r"other-src");
4132            let config = Config::load().unwrap();
4133            assert_eq!(
4134                config,
4135                Config {
4136                    src: "other-src".into(),
4137                    out: "myout".into(),
4138                    verbosity: 3,
4139                    ..Config::default().normalized_optimizer_settings()
4140                }
4141            );
4142
4143            jail.set_env("FOUNDRY_PROFILE", "foo");
4144            let val: Result<String, _> = Config::figment().extract_inner("profile");
4145            assert!(val.is_err());
4146
4147            Ok(())
4148        });
4149    }
4150
4151    #[test]
4152    fn test_extract_basic() {
4153        figment::Jail::expect_with(|jail| {
4154            jail.create_file(
4155                "foundry.toml",
4156                r#"
4157                [profile.default]
4158                src = "mysrc"
4159                out = "myout"
4160                verbosity = 3
4161                evm_version = 'berlin'
4162
4163                [profile.other]
4164                src = "other-src"
4165            "#,
4166            )?;
4167            let loaded = Config::load().unwrap();
4168            assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4169            let base = loaded.into_basic();
4170            let default = Config::default();
4171            assert_eq!(
4172                base,
4173                BasicConfig {
4174                    profile: Config::DEFAULT_PROFILE,
4175                    src: "mysrc".into(),
4176                    out: "myout".into(),
4177                    libs: default.libs.clone(),
4178                    remappings: default.remappings.clone(),
4179                }
4180            );
4181            jail.set_env("FOUNDRY_PROFILE", r"other");
4182            let base = Config::figment().extract::<BasicConfig>().unwrap();
4183            assert_eq!(
4184                base,
4185                BasicConfig {
4186                    profile: Config::DEFAULT_PROFILE,
4187                    src: "other-src".into(),
4188                    out: "myout".into(),
4189                    libs: default.libs.clone(),
4190                    remappings: default.remappings,
4191                }
4192            );
4193            Ok(())
4194        });
4195    }
4196
4197    #[test]
4198    #[should_panic]
4199    fn test_parse_invalid_fuzz_weight() {
4200        figment::Jail::expect_with(|jail| {
4201            jail.create_file(
4202                "foundry.toml",
4203                r"
4204                [fuzz]
4205                dictionary_weight = 101
4206            ",
4207            )?;
4208            let _config = Config::load().unwrap();
4209            Ok(())
4210        });
4211    }
4212
4213    #[test]
4214    fn test_fallback_provider() {
4215        figment::Jail::expect_with(|jail| {
4216            jail.create_file(
4217                "foundry.toml",
4218                r"
4219                [fuzz]
4220                runs = 1
4221                include_storage = false
4222                dictionary_weight = 99
4223
4224                [invariant]
4225                runs = 420
4226
4227                [profile.ci.fuzz]
4228                dictionary_weight = 5
4229
4230                [profile.ci.invariant]
4231                runs = 400
4232            ",
4233            )?;
4234
4235            let invariant_default = InvariantConfig::default();
4236            let config = Config::load().unwrap();
4237
4238            assert_ne!(config.invariant.runs, config.fuzz.runs);
4239            assert_eq!(config.invariant.runs, 420);
4240
4241            assert_ne!(
4242                config.fuzz.dictionary.include_storage,
4243                invariant_default.dictionary.include_storage
4244            );
4245            assert_eq!(
4246                config.invariant.dictionary.include_storage,
4247                config.fuzz.dictionary.include_storage
4248            );
4249
4250            assert_ne!(
4251                config.fuzz.dictionary.dictionary_weight,
4252                invariant_default.dictionary.dictionary_weight
4253            );
4254            assert_eq!(
4255                config.invariant.dictionary.dictionary_weight,
4256                config.fuzz.dictionary.dictionary_weight
4257            );
4258
4259            jail.set_env("FOUNDRY_PROFILE", "ci");
4260            let ci_config = Config::load().unwrap();
4261            assert_eq!(ci_config.fuzz.runs, 1);
4262            assert_eq!(ci_config.invariant.runs, 400);
4263            assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4264            assert_eq!(
4265                ci_config.invariant.dictionary.dictionary_weight,
4266                config.fuzz.dictionary.dictionary_weight
4267            );
4268
4269            Ok(())
4270        })
4271    }
4272
4273    #[test]
4274    fn test_standalone_profile_sections() {
4275        figment::Jail::expect_with(|jail| {
4276            jail.create_file(
4277                "foundry.toml",
4278                r"
4279                [fuzz]
4280                runs = 100
4281
4282                [invariant]
4283                runs = 120
4284
4285                [profile.ci.fuzz]
4286                runs = 420
4287
4288                [profile.ci.invariant]
4289                runs = 500
4290            ",
4291            )?;
4292
4293            let config = Config::load().unwrap();
4294            assert_eq!(config.fuzz.runs, 100);
4295            assert_eq!(config.invariant.runs, 120);
4296
4297            jail.set_env("FOUNDRY_PROFILE", "ci");
4298            let config = Config::load().unwrap();
4299            assert_eq!(config.fuzz.runs, 420);
4300            assert_eq!(config.invariant.runs, 500);
4301
4302            Ok(())
4303        });
4304    }
4305
4306    #[test]
4307    fn can_handle_deviating_dapp_aliases() {
4308        figment::Jail::expect_with(|jail| {
4309            let addr = Address::ZERO;
4310            jail.set_env("DAPP_TEST_NUMBER", 1337);
4311            jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4312            jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4313            jail.set_env("DAPP_TEST_DEPTH", 20);
4314            jail.set_env("DAPP_FORK_BLOCK", 100);
4315            jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4316            jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4317
4318            let config = Config::load().unwrap();
4319
4320            assert_eq!(config.block_number, U256::from(1337));
4321            assert_eq!(config.sender, addr);
4322            assert_eq!(config.fuzz.runs, 420);
4323            assert_eq!(config.invariant.depth, 20);
4324            assert_eq!(config.fork_block_number, Some(100));
4325            assert_eq!(config.optimizer_runs, Some(999));
4326            assert!(!config.optimizer.unwrap());
4327
4328            Ok(())
4329        });
4330    }
4331
4332    #[test]
4333    fn can_parse_libraries() {
4334        figment::Jail::expect_with(|jail| {
4335            jail.set_env(
4336                "DAPP_LIBRARIES",
4337                "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4338            );
4339            let config = Config::load().unwrap();
4340            assert_eq!(
4341                config.libraries,
4342                vec![
4343                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4344                        .to_string()
4345                ]
4346            );
4347
4348            jail.set_env(
4349                "DAPP_LIBRARIES",
4350                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4351            );
4352            let config = Config::load().unwrap();
4353            assert_eq!(
4354                config.libraries,
4355                vec![
4356                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4357                        .to_string(),
4358                ]
4359            );
4360
4361            jail.set_env(
4362                "DAPP_LIBRARIES",
4363                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4364            );
4365            let config = Config::load().unwrap();
4366            assert_eq!(
4367                config.libraries,
4368                vec![
4369                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4370                        .to_string(),
4371                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4372                        .to_string()
4373                ]
4374            );
4375
4376            Ok(())
4377        });
4378    }
4379
4380    #[test]
4381    fn test_parse_many_libraries() {
4382        figment::Jail::expect_with(|jail| {
4383            jail.create_file(
4384                "foundry.toml",
4385                r"
4386                [profile.default]
4387               libraries= [
4388                        './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4389                        './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4390                        './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4391                        './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4392                        './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4393                    ]
4394            ",
4395            )?;
4396            let config = Config::load().unwrap();
4397
4398            let libs = config.parsed_libraries().unwrap().libs;
4399
4400            similar_asserts::assert_eq!(
4401                libs,
4402                BTreeMap::from([
4403                    (
4404                        PathBuf::from("./src/SizeAuctionDiscount.sol"),
4405                        BTreeMap::from([
4406                            (
4407                                "Chainlink".to_string(),
4408                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4409                            ),
4410                            (
4411                                "Math".to_string(),
4412                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4413                            )
4414                        ])
4415                    ),
4416                    (
4417                        PathBuf::from("./src/SizeAuction.sol"),
4418                        BTreeMap::from([
4419                            (
4420                                "ChainlinkTWAP".to_string(),
4421                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4422                            ),
4423                            (
4424                                "Math".to_string(),
4425                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4426                            )
4427                        ])
4428                    ),
4429                    (
4430                        PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4431                        BTreeMap::from([(
4432                            "ChainlinkTWAP".to_string(),
4433                            "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4434                        )])
4435                    ),
4436                ])
4437            );
4438
4439            Ok(())
4440        });
4441    }
4442
4443    #[test]
4444    fn config_roundtrip() {
4445        figment::Jail::expect_with(|jail| {
4446            let default = Config::default().normalized_optimizer_settings();
4447            let basic = default.clone().into_basic();
4448            jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4449
4450            let mut other = Config::load().unwrap();
4451            clear_warning(&mut other);
4452            assert_eq!(default, other);
4453
4454            let other = other.into_basic();
4455            assert_eq!(basic, other);
4456
4457            jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4458            let mut other = Config::load().unwrap();
4459            clear_warning(&mut other);
4460            assert_eq!(default, other);
4461
4462            Ok(())
4463        });
4464    }
4465
4466    #[test]
4467    fn test_fs_permissions() {
4468        figment::Jail::expect_with(|jail| {
4469            jail.create_file(
4470                "foundry.toml",
4471                r#"
4472                [profile.default]
4473                fs_permissions = [{ access = "read-write", path = "./"}]
4474            "#,
4475            )?;
4476            let loaded = Config::load().unwrap();
4477
4478            assert_eq!(
4479                loaded.fs_permissions,
4480                FsPermissions::new(vec![PathPermission::read_write("./")])
4481            );
4482
4483            jail.create_file(
4484                "foundry.toml",
4485                r#"
4486                [profile.default]
4487                fs_permissions = [{ access = "none", path = "./"}]
4488            "#,
4489            )?;
4490            let loaded = Config::load().unwrap();
4491            assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4492
4493            Ok(())
4494        });
4495    }
4496
4497    #[test]
4498    fn test_optimizer_settings_basic() {
4499        figment::Jail::expect_with(|jail| {
4500            jail.create_file(
4501                "foundry.toml",
4502                r"
4503                [profile.default]
4504                optimizer = true
4505
4506                [profile.default.optimizer_details]
4507                yul = false
4508
4509                [profile.default.optimizer_details.yulDetails]
4510                stackAllocation = true
4511            ",
4512            )?;
4513            let mut loaded = Config::load().unwrap();
4514            clear_warning(&mut loaded);
4515            assert_eq!(
4516                loaded.optimizer_details,
4517                Some(OptimizerDetails {
4518                    yul: Some(false),
4519                    yul_details: Some(YulDetails {
4520                        stack_allocation: Some(true),
4521                        ..Default::default()
4522                    }),
4523                    ..Default::default()
4524                })
4525            );
4526
4527            let s = loaded.to_string_pretty().unwrap();
4528            jail.create_file("foundry.toml", &s)?;
4529
4530            let mut reloaded = Config::load().unwrap();
4531            clear_warning(&mut reloaded);
4532            assert_eq!(loaded, reloaded);
4533
4534            Ok(())
4535        });
4536    }
4537
4538    #[test]
4539    fn test_model_checker_settings_basic() {
4540        figment::Jail::expect_with(|jail| {
4541            jail.create_file(
4542                "foundry.toml",
4543                r"
4544                [profile.default]
4545
4546                [profile.default.model_checker]
4547                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4548                engine = 'chc'
4549                targets = [ 'assert', 'outOfBounds' ]
4550                timeout = 10000
4551            ",
4552            )?;
4553            let mut loaded = Config::load().unwrap();
4554            clear_warning(&mut loaded);
4555            assert_eq!(
4556                loaded.model_checker,
4557                Some(ModelCheckerSettings {
4558                    contracts: BTreeMap::from([
4559                        ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4560                        ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4561                    ]),
4562                    engine: Some(ModelCheckerEngine::CHC),
4563                    targets: Some(vec![
4564                        ModelCheckerTarget::Assert,
4565                        ModelCheckerTarget::OutOfBounds
4566                    ]),
4567                    timeout: Some(10000),
4568                    invariants: None,
4569                    show_unproved: None,
4570                    div_mod_with_slacks: None,
4571                    solvers: None,
4572                    show_unsupported: None,
4573                    show_proved_safe: None,
4574                })
4575            );
4576
4577            let s = loaded.to_string_pretty().unwrap();
4578            jail.create_file("foundry.toml", &s)?;
4579
4580            let mut reloaded = Config::load().unwrap();
4581            clear_warning(&mut reloaded);
4582            assert_eq!(loaded, reloaded);
4583
4584            Ok(())
4585        });
4586    }
4587
4588    #[test]
4589    fn test_model_checker_settings_relative_paths() {
4590        figment::Jail::expect_with(|jail| {
4591            jail.create_file(
4592                "foundry.toml",
4593                r"
4594                [profile.default]
4595
4596                [profile.default.model_checker]
4597                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4598                engine = 'chc'
4599                targets = [ 'assert', 'outOfBounds' ]
4600                timeout = 10000
4601            ",
4602            )?;
4603            let loaded = Config::load().unwrap().sanitized();
4604
4605            // NOTE(onbjerg): We have to canonicalize the path here using dunce because figment will
4606            // canonicalize the jail path using the standard library. The standard library *always*
4607            // transforms Windows paths to some weird extended format, which none of our code base
4608            // does.
4609            let dir = foundry_compilers::utils::canonicalize(jail.directory())
4610                .expect("Could not canonicalize jail path");
4611            assert_eq!(
4612                loaded.model_checker,
4613                Some(ModelCheckerSettings {
4614                    contracts: BTreeMap::from([
4615                        (
4616                            format!("{}", dir.join("a.sol").display()),
4617                            vec!["A1".to_string(), "A2".to_string()]
4618                        ),
4619                        (
4620                            format!("{}", dir.join("b.sol").display()),
4621                            vec!["B1".to_string(), "B2".to_string()]
4622                        ),
4623                    ]),
4624                    engine: Some(ModelCheckerEngine::CHC),
4625                    targets: Some(vec![
4626                        ModelCheckerTarget::Assert,
4627                        ModelCheckerTarget::OutOfBounds
4628                    ]),
4629                    timeout: Some(10000),
4630                    invariants: None,
4631                    show_unproved: None,
4632                    div_mod_with_slacks: None,
4633                    solvers: None,
4634                    show_unsupported: None,
4635                    show_proved_safe: None,
4636                })
4637            );
4638
4639            Ok(())
4640        });
4641    }
4642
4643    #[test]
4644    fn test_fmt_config() {
4645        figment::Jail::expect_with(|jail| {
4646            jail.create_file(
4647                "foundry.toml",
4648                r#"
4649                [fmt]
4650                line_length = 100
4651                tab_width = 2
4652                bracket_spacing = true
4653                style = "space"
4654            "#,
4655            )?;
4656            let loaded = Config::load().unwrap().sanitized();
4657            assert_eq!(
4658                loaded.fmt,
4659                FormatterConfig {
4660                    line_length: 100,
4661                    tab_width: 2,
4662                    bracket_spacing: true,
4663                    style: IndentStyle::Space,
4664                    ..Default::default()
4665                }
4666            );
4667
4668            Ok(())
4669        });
4670    }
4671
4672    #[test]
4673    fn test_lint_config() {
4674        figment::Jail::expect_with(|jail| {
4675            jail.create_file(
4676                "foundry.toml",
4677                r"
4678                [lint]
4679                severity = ['high', 'medium']
4680                exclude_lints = ['incorrect-shift']
4681                ",
4682            )?;
4683            let loaded = Config::load().unwrap().sanitized();
4684            assert_eq!(
4685                loaded.lint,
4686                LinterConfig {
4687                    severity: vec![LintSeverity::High, LintSeverity::Med],
4688                    exclude_lints: vec!["incorrect-shift".into()],
4689                    ..Default::default()
4690                }
4691            );
4692
4693            Ok(())
4694        });
4695    }
4696
4697    #[test]
4698    fn test_invariant_config() {
4699        figment::Jail::expect_with(|jail| {
4700            jail.create_file(
4701                "foundry.toml",
4702                r"
4703                [invariant]
4704                runs = 512
4705                depth = 10
4706            ",
4707            )?;
4708
4709            let loaded = Config::load().unwrap().sanitized();
4710            assert_eq!(
4711                loaded.invariant,
4712                InvariantConfig {
4713                    runs: 512,
4714                    depth: 10,
4715                    failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4716                    ..Default::default()
4717                }
4718            );
4719
4720            Ok(())
4721        });
4722    }
4723
4724    #[test]
4725    fn test_standalone_sections_env() {
4726        figment::Jail::expect_with(|jail| {
4727            jail.create_file(
4728                "foundry.toml",
4729                r"
4730                [fuzz]
4731                runs = 100
4732
4733                [invariant]
4734                depth = 1
4735            ",
4736            )?;
4737
4738            jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4739            jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4740            jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4741
4742            let config = Config::load().unwrap();
4743            assert_eq!(config.fmt.line_length, 95);
4744            assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4745            assert_eq!(config.invariant.depth, 5);
4746
4747            Ok(())
4748        });
4749    }
4750
4751    #[test]
4752    fn test_parse_with_profile() {
4753        let foundry_str = r"
4754            [profile.default]
4755            src = 'src'
4756            out = 'out'
4757            libs = ['lib']
4758
4759            # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4760        ";
4761        assert_eq!(
4762            parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4763            (
4764                Config::DEFAULT_PROFILE,
4765                BasicConfig {
4766                    profile: Config::DEFAULT_PROFILE,
4767                    src: "src".into(),
4768                    out: "out".into(),
4769                    libs: vec!["lib".into()],
4770                    remappings: vec![]
4771                }
4772            )
4773        );
4774    }
4775
4776    #[test]
4777    fn test_implicit_profile_loads() {
4778        figment::Jail::expect_with(|jail| {
4779            jail.create_file(
4780                "foundry.toml",
4781                r"
4782                [default]
4783                src = 'my-src'
4784                out = 'my-out'
4785            ",
4786            )?;
4787            let loaded = Config::load().unwrap().sanitized();
4788            assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4789            assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4790            assert_eq!(
4791                loaded.warnings,
4792                vec![Warning::UnknownSection {
4793                    unknown_section: Profile::new("default"),
4794                    source: Some("foundry.toml".into())
4795                }]
4796            );
4797
4798            Ok(())
4799        });
4800    }
4801
4802    #[test]
4803    fn test_etherscan_api_key() {
4804        figment::Jail::expect_with(|jail| {
4805            jail.create_file(
4806                "foundry.toml",
4807                r"
4808                [default]
4809            ",
4810            )?;
4811            jail.set_env("ETHERSCAN_API_KEY", "");
4812            let loaded = Config::load().unwrap().sanitized();
4813            assert!(loaded.etherscan_api_key.is_none());
4814
4815            jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4816            let loaded = Config::load().unwrap().sanitized();
4817            assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4818
4819            Ok(())
4820        });
4821    }
4822
4823    #[test]
4824    fn test_etherscan_api_key_figment() {
4825        figment::Jail::expect_with(|jail| {
4826            jail.create_file(
4827                "foundry.toml",
4828                r"
4829                [default]
4830                etherscan_api_key = 'DUMMY'
4831            ",
4832            )?;
4833            jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4834
4835            let figment = Config::figment_with_root(jail.directory())
4836                .merge(("etherscan_api_key", "USER_KEY"));
4837
4838            let loaded = Config::from_provider(figment).unwrap();
4839            assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4840
4841            Ok(())
4842        });
4843    }
4844
4845    #[test]
4846    fn test_normalize_defaults() {
4847        figment::Jail::expect_with(|jail| {
4848            jail.create_file(
4849                "foundry.toml",
4850                r"
4851                [default]
4852                solc = '0.8.13'
4853            ",
4854            )?;
4855
4856            let loaded = Config::load().unwrap().sanitized();
4857            assert_eq!(loaded.evm_version, EvmVersion::London);
4858            Ok(())
4859        });
4860    }
4861
4862    // a test to print the config, mainly used to update the example config in the README
4863    #[expect(clippy::disallowed_macros)]
4864    #[test]
4865    #[ignore]
4866    fn print_config() {
4867        let config = Config {
4868            optimizer_details: Some(OptimizerDetails {
4869                peephole: None,
4870                inliner: None,
4871                jumpdest_remover: None,
4872                order_literals: None,
4873                deduplicate: None,
4874                cse: None,
4875                constant_optimizer: Some(true),
4876                yul: Some(true),
4877                yul_details: Some(YulDetails {
4878                    stack_allocation: None,
4879                    optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4880                }),
4881                simple_counter_for_loop_unchecked_increment: None,
4882            }),
4883            ..Default::default()
4884        };
4885        println!("{}", config.to_string_pretty().unwrap());
4886    }
4887
4888    #[test]
4889    fn can_use_impl_figment_macro() {
4890        #[derive(Default, Serialize)]
4891        struct MyArgs {
4892            #[serde(skip_serializing_if = "Option::is_none")]
4893            root: Option<PathBuf>,
4894        }
4895        impl_figment_convert!(MyArgs);
4896
4897        impl Provider for MyArgs {
4898            fn metadata(&self) -> Metadata {
4899                Metadata::default()
4900            }
4901
4902            fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4903                let value = Value::serialize(self)?;
4904                let error = InvalidType(value.to_actual(), "map".into());
4905                let dict = value.into_dict().ok_or(error)?;
4906                Ok(Map::from([(Config::selected_profile(), dict)]))
4907            }
4908        }
4909
4910        let _figment: Figment = From::from(&MyArgs::default());
4911
4912        #[derive(Default)]
4913        struct Outer {
4914            start: MyArgs,
4915            other: MyArgs,
4916            another: MyArgs,
4917        }
4918        impl_figment_convert!(Outer, start, other, another);
4919
4920        let _figment: Figment = From::from(&Outer::default());
4921    }
4922
4923    #[test]
4924    fn list_cached_blocks() -> eyre::Result<()> {
4925        fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4926            let block_path = chain_path.join(block_number);
4927            fs::create_dir(block_path.as_path()).unwrap();
4928            let file_path = block_path.join("storage.json");
4929            let mut file = File::create(file_path).unwrap();
4930            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4931        }
4932
4933        fn fake_block_cache_block_path_as_file(
4934            chain_path: &Path,
4935            block_number: &str,
4936            size_bytes: usize,
4937        ) {
4938            let block_path = chain_path.join(block_number);
4939            let mut file = File::create(block_path).unwrap();
4940            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4941        }
4942
4943        let chain_dir = tempdir()?;
4944
4945        fake_block_cache(chain_dir.path(), "1", 100);
4946        fake_block_cache(chain_dir.path(), "2", 500);
4947        fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4948        // Pollution file that should not show up in the cached block
4949        let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4950        writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4951
4952        let result = Config::get_cached_blocks(chain_dir.path())?;
4953
4954        assert_eq!(result.len(), 3);
4955        let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4956        let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4957        let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4958
4959        assert_eq!(block1.0, "1");
4960        assert_eq!(block1.1, 100);
4961        assert_eq!(block2.0, "2");
4962        assert_eq!(block2.1, 500);
4963        assert_eq!(block3.0, "3");
4964        assert_eq!(block3.1, 900);
4965
4966        chain_dir.close()?;
4967        Ok(())
4968    }
4969
4970    #[test]
4971    fn list_etherscan_cache() -> eyre::Result<()> {
4972        fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4973            let metadata_path = chain_path.join("sources");
4974            let abi_path = chain_path.join("abi");
4975            let _ = fs::create_dir(metadata_path.as_path());
4976            let _ = fs::create_dir(abi_path.as_path());
4977
4978            let metadata_file_path = metadata_path.join(address);
4979            let mut metadata_file = File::create(metadata_file_path).unwrap();
4980            writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4981                .unwrap();
4982
4983            let abi_file_path = abi_path.join(address);
4984            let mut abi_file = File::create(abi_file_path).unwrap();
4985            writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4986                .unwrap();
4987        }
4988
4989        let chain_dir = tempdir()?;
4990
4991        fake_etherscan_cache(chain_dir.path(), "1", 100);
4992        fake_etherscan_cache(chain_dir.path(), "2", 500);
4993
4994        let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4995
4996        assert_eq!(result, 600);
4997
4998        chain_dir.close()?;
4999        Ok(())
5000    }
5001
5002    #[test]
5003    fn test_parse_error_codes() {
5004        figment::Jail::expect_with(|jail| {
5005            jail.create_file(
5006                "foundry.toml",
5007                r#"
5008                [default]
5009                ignored_error_codes = ["license", "unreachable", 1337]
5010            "#,
5011            )?;
5012
5013            let config = Config::load().unwrap();
5014            assert_eq!(
5015                config.ignored_error_codes,
5016                vec![
5017                    SolidityErrorCode::SpdxLicenseNotProvided,
5018                    SolidityErrorCode::Unreachable,
5019                    SolidityErrorCode::Other(1337)
5020                ]
5021            );
5022
5023            Ok(())
5024        });
5025    }
5026
5027    #[test]
5028    fn test_parse_file_paths() {
5029        figment::Jail::expect_with(|jail| {
5030            jail.create_file(
5031                "foundry.toml",
5032                r#"
5033                [default]
5034                ignored_warnings_from = ["something"]
5035            "#,
5036            )?;
5037
5038            let config = Config::load().unwrap();
5039            assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
5040
5041            Ok(())
5042        });
5043    }
5044
5045    #[test]
5046    fn test_parse_optimizer_settings() {
5047        figment::Jail::expect_with(|jail| {
5048            jail.create_file(
5049                "foundry.toml",
5050                r"
5051                [default]
5052                [profile.default.optimizer_details]
5053            ",
5054            )?;
5055
5056            let config = Config::load().unwrap();
5057            assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
5058
5059            Ok(())
5060        });
5061    }
5062
5063    #[test]
5064    fn test_parse_labels() {
5065        figment::Jail::expect_with(|jail| {
5066            jail.create_file(
5067                "foundry.toml",
5068                r#"
5069                [labels]
5070                0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
5071                0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
5072            "#,
5073            )?;
5074
5075            let config = Config::load().unwrap();
5076            assert_eq!(
5077                config.labels,
5078                AddressHashMap::from_iter(vec![
5079                    (
5080                        address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
5081                        "Uniswap V3: Factory".to_string()
5082                    ),
5083                    (
5084                        address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
5085                        "Uniswap V3: Positions NFT".to_string()
5086                    ),
5087                ])
5088            );
5089
5090            Ok(())
5091        });
5092    }
5093
5094    #[test]
5095    fn test_parse_vyper() {
5096        figment::Jail::expect_with(|jail| {
5097            jail.create_file(
5098                "foundry.toml",
5099                r#"
5100                [vyper]
5101                optimize = "codesize"
5102                path = "/path/to/vyper"
5103                experimental_codegen = true
5104            "#,
5105            )?;
5106
5107            let config = Config::load().unwrap();
5108            assert_eq!(
5109                config.vyper,
5110                VyperConfig {
5111                    optimize: Some(VyperOptimizationMode::Codesize),
5112                    path: Some("/path/to/vyper".into()),
5113                    experimental_codegen: Some(true),
5114                }
5115            );
5116
5117            Ok(())
5118        });
5119    }
5120
5121    #[test]
5122    fn test_parse_soldeer() {
5123        figment::Jail::expect_with(|jail| {
5124            jail.create_file(
5125                "foundry.toml",
5126                r#"
5127                [soldeer]
5128                remappings_generate = true
5129                remappings_regenerate = false
5130                remappings_version = true
5131                remappings_prefix = "@"
5132                remappings_location = "txt"
5133                recursive_deps = true
5134            "#,
5135            )?;
5136
5137            let config = Config::load().unwrap();
5138
5139            assert_eq!(
5140                config.soldeer,
5141                Some(SoldeerConfig {
5142                    remappings_generate: true,
5143                    remappings_regenerate: false,
5144                    remappings_version: true,
5145                    remappings_prefix: "@".to_string(),
5146                    remappings_location: RemappingsLocation::Txt,
5147                    recursive_deps: true,
5148                })
5149            );
5150
5151            Ok(())
5152        });
5153    }
5154
5155    // <https://github.com/foundry-rs/foundry/issues/10926>
5156    #[test]
5157    fn test_resolve_mesc_by_chain_id() {
5158        let s = r#"{
5159    "mesc_version": "0.2.1",
5160    "default_endpoint": null,
5161    "endpoints": {
5162        "sophon_50104": {
5163            "name": "sophon_50104",
5164            "url": "https://rpc.sophon.xyz",
5165            "chain_id": "50104",
5166            "endpoint_metadata": {}
5167        }
5168    },
5169    "network_defaults": {
5170    },
5171    "network_names": {},
5172    "profiles": {
5173        "foundry": {
5174            "name": "foundry",
5175            "default_endpoint": "local_ethereum",
5176            "network_defaults": {
5177                "50104": "sophon_50104"
5178            },
5179            "profile_metadata": {},
5180            "use_mesc": true
5181        }
5182    },
5183    "global_metadata": {}
5184}"#;
5185
5186        let config = serde_json::from_str(s).unwrap();
5187        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5188            .unwrap()
5189            .unwrap();
5190        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5191
5192        let s = r#"{
5193    "mesc_version": "0.2.1",
5194    "default_endpoint": null,
5195    "endpoints": {
5196        "sophon_50104": {
5197            "name": "sophon_50104",
5198            "url": "https://rpc.sophon.xyz",
5199            "chain_id": "50104",
5200            "endpoint_metadata": {}
5201        }
5202    },
5203    "network_defaults": {
5204        "50104": "sophon_50104"
5205    },
5206    "network_names": {},
5207    "profiles": {},
5208    "global_metadata": {}
5209}"#;
5210
5211        let config = serde_json::from_str(s).unwrap();
5212        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5213            .unwrap()
5214            .unwrap();
5215        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5216    }
5217
5218    #[test]
5219    fn test_get_etherscan_config_with_unknown_chain() {
5220        figment::Jail::expect_with(|jail| {
5221            jail.create_file(
5222                "foundry.toml",
5223                r#"
5224                [etherscan]
5225                mainnet = { chain = 3658348, key = "api-key"}
5226            "#,
5227            )?;
5228            let config = Config::load().unwrap();
5229            let unknown_chain = Chain::from_id(3658348);
5230            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5231            assert!(result.is_err());
5232            let error_msg = result.unwrap_err().to_string();
5233            assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
5234            assert!(error_msg.contains("Specify a `url`"));
5235            assert!(error_msg.contains("Verify the chain `3658348` is correct"));
5236
5237            Ok(())
5238        });
5239    }
5240
5241    #[test]
5242    fn test_get_etherscan_config_with_existing_chain_and_url() {
5243        figment::Jail::expect_with(|jail| {
5244            jail.create_file(
5245                "foundry.toml",
5246                r#"
5247                [etherscan]
5248                mainnet = { chain = 1, key = "api-key" }
5249            "#,
5250            )?;
5251            let config = Config::load().unwrap();
5252            let unknown_chain = Chain::from_id(1);
5253            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5254            assert!(result.is_ok());
5255            Ok(())
5256        });
5257    }
5258
5259    #[test]
5260    fn test_can_inherit_a_base_toml() {
5261        figment::Jail::expect_with(|jail| {
5262            // Create base config file with optimizer_runs = 800
5263            jail.create_file(
5264                "base-config.toml",
5265                r#"
5266                    [profile.default]
5267                    optimizer_runs = 800
5268
5269                    [invariant]
5270                    runs = 1000
5271
5272                    [rpc_endpoints]
5273                    mainnet = "https://example.com"
5274                    optimism = "https://example-2.com/"
5275                    "#,
5276            )?;
5277
5278            // Create local config that inherits from base-config.toml
5279            jail.create_file(
5280                "foundry.toml",
5281                r#"
5282                    [profile.default]
5283                    extends = "base-config.toml"
5284
5285                    [invariant]
5286                    runs = 333
5287                    depth = 15
5288
5289                    [rpc_endpoints]
5290                    mainnet = "https://test.xyz/rpc"
5291                    "#,
5292            )?;
5293
5294            let config = Config::load().unwrap();
5295            assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
5296
5297            // optimizer_runs should be inherited from base-config.toml
5298            assert_eq!(config.optimizer_runs, Some(800));
5299
5300            // invariant settings should be overridden by local config
5301            assert_eq!(config.invariant.runs, 333);
5302            assert_eq!(config.invariant.depth, 15);
5303
5304            // rpc_endpoints.mainnet should be overridden by local config
5305            // optimism should be inherited from base config
5306            let endpoints = config.rpc_endpoints.resolved();
5307            assert!(
5308                endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
5309            );
5310            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
5311
5312            Ok(())
5313        });
5314    }
5315
5316    #[test]
5317    fn test_inheritance_validation() {
5318        figment::Jail::expect_with(|jail| {
5319            // Test 1: Base file with 'extends' should fail
5320            jail.create_file(
5321                "base-with-inherit.toml",
5322                r#"
5323                    [profile.default]
5324                    extends = "another.toml"
5325                    optimizer_runs = 800
5326                    "#,
5327            )?;
5328
5329            jail.create_file(
5330                "foundry.toml",
5331                r#"
5332                    [profile.default]
5333                    extends = "base-with-inherit.toml"
5334                    "#,
5335            )?;
5336
5337            // Should fail because base file has 'extends'
5338            let result = Config::load();
5339            assert!(result.is_err());
5340            assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
5341
5342            // Test 2: Circular reference should fail
5343            jail.create_file(
5344                "foundry.toml",
5345                r#"
5346                    [profile.default]
5347                    extends = "foundry.toml"
5348                    "#,
5349            )?;
5350
5351            let result = Config::load();
5352            assert!(result.is_err());
5353            assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
5354
5355            // Test 3: Non-existent base file should fail
5356            jail.create_file(
5357                "foundry.toml",
5358                r#"
5359                    [profile.default]
5360                    extends = "non-existent.toml"
5361                    "#,
5362            )?;
5363
5364            let result = Config::load();
5365            assert!(result.is_err());
5366            let err_msg = result.unwrap_err().to_string();
5367            assert!(
5368                err_msg.contains("does not exist")
5369                    || err_msg.contains("Failed to resolve inherited config path"),
5370                "Error message: {err_msg}"
5371            );
5372
5373            Ok(())
5374        });
5375    }
5376
5377    #[test]
5378    fn test_complex_inheritance_merging() {
5379        figment::Jail::expect_with(|jail| {
5380            // Create a comprehensive base config
5381            jail.create_file(
5382                "base.toml",
5383                r#"
5384                    [profile.default]
5385                    optimizer = true
5386                    optimizer_runs = 1000
5387                    via_ir = false
5388                    solc = "0.8.19"
5389
5390                    [invariant]
5391                    runs = 500
5392                    depth = 100
5393
5394                    [fuzz]
5395                    runs = 256
5396                    seed = "0x123"
5397
5398                    [rpc_endpoints]
5399                    mainnet = "https://base-mainnet.com"
5400                    optimism = "https://base-optimism.com"
5401                    arbitrum = "https://base-arbitrum.com"
5402                    "#,
5403            )?;
5404
5405            // Create local config that overrides some values
5406            jail.create_file(
5407                "foundry.toml",
5408                r#"
5409                    [profile.default]
5410                    extends = "base.toml"
5411                    optimizer_runs = 200  # Override
5412                    via_ir = true        # Override
5413                    # optimizer and solc are inherited
5414
5415                    [invariant]
5416                    runs = 333  # Override
5417                    # depth is inherited
5418
5419                    # fuzz section is fully inherited
5420
5421                    [rpc_endpoints]
5422                    mainnet = "https://local-mainnet.com"  # Override
5423                    # optimism and arbitrum are inherited
5424                    polygon = "https://local-polygon.com"  # New
5425                    "#,
5426            )?;
5427
5428            let config = Config::load().unwrap();
5429
5430            // Check profile.default values
5431            assert_eq!(config.optimizer, Some(true));
5432            assert_eq!(config.optimizer_runs, Some(200));
5433            assert_eq!(config.via_ir, true);
5434            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5435
5436            // Check invariant section
5437            assert_eq!(config.invariant.runs, 333);
5438            assert_eq!(config.invariant.depth, 100);
5439
5440            // Check fuzz section (fully inherited)
5441            assert_eq!(config.fuzz.runs, 256);
5442            assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
5443
5444            // Check rpc_endpoints
5445            let endpoints = config.rpc_endpoints.resolved();
5446            assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
5447            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
5448            assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
5449            assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
5450
5451            Ok(())
5452        });
5453    }
5454
5455    #[test]
5456    fn test_inheritance_with_different_profiles() {
5457        figment::Jail::expect_with(|jail| {
5458            // Create base config with multiple profiles
5459            jail.create_file(
5460                "base.toml",
5461                r#"
5462                    [profile.default]
5463                    optimizer = true
5464                    optimizer_runs = 200
5465
5466                    [profile.ci]
5467                    optimizer = true
5468                    optimizer_runs = 10000
5469                    via_ir = true
5470
5471                    [profile.dev]
5472                    optimizer = false
5473                    "#,
5474            )?;
5475
5476            // Local config inherits from base - only for default profile
5477            jail.create_file(
5478                "foundry.toml",
5479                r#"
5480                    [profile.default]
5481                    extends = "base.toml"
5482                    verbosity = 3
5483
5484                    [profile.ci]
5485                    optimizer_runs = 5000  # This doesn't inherit from base.toml's ci profile
5486                    "#,
5487            )?;
5488
5489            // Test default profile
5490            let config = Config::load().unwrap();
5491            assert_eq!(config.optimizer, Some(true));
5492            assert_eq!(config.optimizer_runs, Some(200));
5493            assert_eq!(config.verbosity, 3);
5494
5495            // Test CI profile (NO 'extends', so doesn't inherit from base)
5496            jail.set_env("FOUNDRY_PROFILE", "ci");
5497            let config = Config::load().unwrap();
5498            assert_eq!(config.optimizer_runs, Some(5000));
5499            assert_eq!(config.optimizer, Some(true));
5500            // via_ir is not set in local ci profile and there's no 'extends', so default
5501            assert_eq!(config.via_ir, false);
5502
5503            Ok(())
5504        });
5505    }
5506
5507    #[test]
5508    fn test_inheritance_with_env_vars() {
5509        figment::Jail::expect_with(|jail| {
5510            jail.create_file(
5511                "base.toml",
5512                r#"
5513                    [profile.default]
5514                    optimizer_runs = 500
5515                    sender = "0x0000000000000000000000000000000000000001"
5516                    verbosity = 1
5517                    "#,
5518            )?;
5519
5520            jail.create_file(
5521                "foundry.toml",
5522                r#"
5523                    [profile.default]
5524                    extends = "base.toml"
5525                    verbosity = 2
5526                    "#,
5527            )?;
5528
5529            // Environment variables should override both base and local values
5530            jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
5531            jail.set_env("FOUNDRY_VERBOSITY", "4");
5532
5533            let config = Config::load().unwrap();
5534            assert_eq!(config.optimizer_runs, Some(999));
5535            assert_eq!(config.verbosity, 4);
5536            assert_eq!(
5537                config.sender,
5538                "0x0000000000000000000000000000000000000001"
5539                    .parse::<alloy_primitives::Address>()
5540                    .unwrap()
5541            );
5542
5543            Ok(())
5544        });
5545    }
5546
5547    #[test]
5548    fn test_inheritance_with_subdirectories() {
5549        figment::Jail::expect_with(|jail| {
5550            // Create base config in a subdirectory
5551            jail.create_dir("configs")?;
5552            jail.create_file(
5553                "configs/base.toml",
5554                r#"
5555                    [profile.default]
5556                    optimizer_runs = 800
5557                    src = "contracts"
5558                    "#,
5559            )?;
5560
5561            // Reference it with relative path
5562            jail.create_file(
5563                "foundry.toml",
5564                r#"
5565                    [profile.default]
5566                    extends = "configs/base.toml"
5567                    test = "tests"
5568                    "#,
5569            )?;
5570
5571            let config = Config::load().unwrap();
5572            assert_eq!(config.optimizer_runs, Some(800));
5573            assert_eq!(config.src, PathBuf::from("contracts"));
5574            assert_eq!(config.test, PathBuf::from("tests"));
5575
5576            // Test with parent directory reference
5577            jail.create_dir("project")?;
5578            jail.create_file(
5579                "shared-base.toml",
5580                r#"
5581                    [profile.default]
5582                    optimizer_runs = 1500
5583                    "#,
5584            )?;
5585
5586            jail.create_file(
5587                "project/foundry.toml",
5588                r#"
5589                    [profile.default]
5590                    extends = "../shared-base.toml"
5591                    "#,
5592            )?;
5593
5594            std::env::set_current_dir(jail.directory().join("project")).unwrap();
5595            let config = Config::load().unwrap();
5596            assert_eq!(config.optimizer_runs, Some(1500));
5597
5598            Ok(())
5599        });
5600    }
5601
5602    #[test]
5603    fn test_inheritance_with_empty_files() {
5604        figment::Jail::expect_with(|jail| {
5605            // Empty base file
5606            jail.create_file(
5607                "base.toml",
5608                r#"
5609                    [profile.default]
5610                    "#,
5611            )?;
5612
5613            jail.create_file(
5614                "foundry.toml",
5615                r#"
5616                    [profile.default]
5617                    extends = "base.toml"
5618                    optimizer_runs = 300
5619                    "#,
5620            )?;
5621
5622            let config = Config::load().unwrap();
5623            assert_eq!(config.optimizer_runs, Some(300));
5624
5625            // Empty local file (only 'extends')
5626            jail.create_file(
5627                "base2.toml",
5628                r#"
5629                    [profile.default]
5630                    optimizer_runs = 400
5631                    via_ir = true
5632                    "#,
5633            )?;
5634
5635            jail.create_file(
5636                "foundry.toml",
5637                r#"
5638                    [profile.default]
5639                    extends = "base2.toml"
5640                    "#,
5641            )?;
5642
5643            let config = Config::load().unwrap();
5644            assert_eq!(config.optimizer_runs, Some(400));
5645            assert!(config.via_ir);
5646
5647            Ok(())
5648        });
5649    }
5650
5651    #[test]
5652    fn test_inheritance_array_and_table_merging() {
5653        figment::Jail::expect_with(|jail| {
5654            jail.create_file(
5655                "base.toml",
5656                r#"
5657                    [profile.default]
5658                    libs = ["lib", "node_modules"]
5659                    ignored_error_codes = [5667, 1878]
5660                    extra_output = ["metadata", "ir"]
5661
5662                    [profile.default.model_checker]
5663                    engine = "chc"
5664                    timeout = 10000
5665                    targets = ["assert"]
5666
5667                    [profile.default.optimizer_details]
5668                    peephole = true
5669                    inliner = true
5670                    "#,
5671            )?;
5672
5673            jail.create_file(
5674                "foundry.toml",
5675                r#"
5676                    [profile.default]
5677                    extends = "base.toml"
5678                    libs = ["custom-lib"]  # Concatenates with base array
5679                    ignored_error_codes = [2018]  # Concatenates with base array
5680
5681                    [profile.default.model_checker]
5682                    timeout = 5000  # Overrides base value
5683                    # engine and targets are inherited
5684
5685                    [profile.default.optimizer_details]
5686                    jumpdest_remover = true  # Adds new field
5687                    # peephole and inliner are inherited
5688                    "#,
5689            )?;
5690
5691            let config = Config::load().unwrap();
5692
5693            // Arrays are now concatenated with admerge (base + local)
5694            assert_eq!(
5695                config.libs,
5696                vec![
5697                    PathBuf::from("lib"),
5698                    PathBuf::from("node_modules"),
5699                    PathBuf::from("custom-lib")
5700                ]
5701            );
5702            assert_eq!(
5703                config.ignored_error_codes,
5704                vec![
5705                    SolidityErrorCode::UnusedFunctionParameter, // 5667 from base.toml
5706                    SolidityErrorCode::SpdxLicenseNotProvided,  // 1878 from base.toml
5707                    SolidityErrorCode::FunctionStateMutabilityCanBeRestricted  // 2018 from local
5708                ]
5709            );
5710
5711            // Tables are deep-merged
5712            assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
5713            assert_eq!(
5714                config.model_checker.as_ref().unwrap().engine,
5715                Some(ModelCheckerEngine::CHC)
5716            );
5717            assert_eq!(
5718                config.model_checker.as_ref().unwrap().targets,
5719                Some(vec![ModelCheckerTarget::Assert])
5720            );
5721
5722            // optimizer_details table is actually merged, not replaced
5723            assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
5724            assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
5725            assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
5726
5727            Ok(())
5728        });
5729    }
5730
5731    #[test]
5732    fn test_inheritance_with_special_sections() {
5733        figment::Jail::expect_with(|jail| {
5734            jail.create_file(
5735                "base.toml",
5736                r#"
5737                    [profile.default]
5738                    # Base file should not have 'extends' to avoid nested inheritance
5739
5740                    [labels]
5741                    "0x0000000000000000000000000000000000000001" = "Alice"
5742                    "0x0000000000000000000000000000000000000002" = "Bob"
5743
5744                    [[profile.default.fs_permissions]]
5745                    access = "read"
5746                    path = "./src"
5747
5748                    [[profile.default.fs_permissions]]
5749                    access = "read-write"
5750                    path = "./cache"
5751                    "#,
5752            )?;
5753
5754            jail.create_file(
5755                "foundry.toml",
5756                r#"
5757                    [profile.default]
5758                    extends = "base.toml"
5759
5760                    [labels]
5761                    "0x0000000000000000000000000000000000000002" = "Bob Updated"
5762                    "0x0000000000000000000000000000000000000003" = "Charlie"
5763
5764                    [[profile.default.fs_permissions]]
5765                    access = "read"
5766                    path = "./test"
5767                    "#,
5768            )?;
5769
5770            let config = Config::load().unwrap();
5771
5772            // Labels should be merged
5773            assert_eq!(
5774                config.labels.get(
5775                    &"0x0000000000000000000000000000000000000001"
5776                        .parse::<alloy_primitives::Address>()
5777                        .unwrap()
5778                ),
5779                Some(&"Alice".to_string())
5780            );
5781            assert_eq!(
5782                config.labels.get(
5783                    &"0x0000000000000000000000000000000000000002"
5784                        .parse::<alloy_primitives::Address>()
5785                        .unwrap()
5786                ),
5787                Some(&"Bob Updated".to_string())
5788            );
5789            assert_eq!(
5790                config.labels.get(
5791                    &"0x0000000000000000000000000000000000000003"
5792                        .parse::<alloy_primitives::Address>()
5793                        .unwrap()
5794                ),
5795                Some(&"Charlie".to_string())
5796            );
5797
5798            // fs_permissions array is now concatenated with addmerge (base + local)
5799            assert_eq!(config.fs_permissions.permissions.len(), 3); // 2 from base + 1 from local
5800            // Check that all permissions are present
5801            assert!(
5802                config
5803                    .fs_permissions
5804                    .permissions
5805                    .iter()
5806                    .any(|p| p.path.to_str().unwrap() == "./src")
5807            );
5808            assert!(
5809                config
5810                    .fs_permissions
5811                    .permissions
5812                    .iter()
5813                    .any(|p| p.path.to_str().unwrap() == "./cache")
5814            );
5815            assert!(
5816                config
5817                    .fs_permissions
5818                    .permissions
5819                    .iter()
5820                    .any(|p| p.path.to_str().unwrap() == "./test")
5821            );
5822
5823            Ok(())
5824        });
5825    }
5826
5827    #[test]
5828    fn test_inheritance_with_compilation_settings() {
5829        figment::Jail::expect_with(|jail| {
5830            jail.create_file(
5831                "base.toml",
5832                r#"
5833                    [profile.default]
5834                    solc = "0.8.19"
5835                    evm_version = "paris"
5836                    via_ir = false
5837                    optimizer = true
5838                    optimizer_runs = 200
5839
5840                    [profile.default.optimizer_details]
5841                    peephole = true
5842                    inliner = false
5843                    jumpdest_remover = true
5844                    order_literals = false
5845                    deduplicate = true
5846                    cse = true
5847                    constant_optimizer = true
5848                    yul = true
5849
5850                    [profile.default.optimizer_details.yul_details]
5851                    stack_allocation = true
5852                    optimizer_steps = "dhfoDgvulfnTUtnIf"
5853                    "#,
5854            )?;
5855
5856            jail.create_file(
5857                "foundry.toml",
5858                r#"
5859                    [profile.default]
5860                    extends = "base.toml"
5861                    evm_version = "shanghai"  # Override
5862                    optimizer_runs = 1000  # Override
5863
5864                    [profile.default.optimizer_details]
5865                    inliner = true  # Override
5866                    # Rest inherited
5867                    "#,
5868            )?;
5869
5870            let config = Config::load().unwrap();
5871
5872            // Check compilation settings
5873            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5874            assert_eq!(config.evm_version, EvmVersion::Shanghai);
5875            assert_eq!(config.via_ir, false);
5876            assert_eq!(config.optimizer, Some(true));
5877            assert_eq!(config.optimizer_runs, Some(1000));
5878
5879            // Check optimizer details - the table is actually merged
5880            let details = config.optimizer_details.as_ref().unwrap();
5881            assert_eq!(details.peephole, Some(true));
5882            assert_eq!(details.inliner, Some(true));
5883            assert_eq!(details.jumpdest_remover, None);
5884            assert_eq!(details.order_literals, None);
5885            assert_eq!(details.deduplicate, Some(true));
5886            assert_eq!(details.cse, Some(true));
5887            assert_eq!(details.constant_optimizer, None);
5888            assert_eq!(details.yul, Some(true));
5889
5890            // Check yul details - inherited from base
5891            if let Some(yul_details) = details.yul_details.as_ref() {
5892                assert_eq!(yul_details.stack_allocation, Some(true));
5893                assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
5894            }
5895
5896            Ok(())
5897        });
5898    }
5899
5900    #[test]
5901    fn test_inheritance_with_remappings() {
5902        figment::Jail::expect_with(|jail| {
5903            jail.create_file(
5904                "base.toml",
5905                r#"
5906                    [profile.default]
5907                    remappings = [
5908                        "forge-std/=lib/forge-std/src/",
5909                        "@openzeppelin/=lib/openzeppelin-contracts/",
5910                        "ds-test/=lib/ds-test/src/"
5911                    ]
5912                    auto_detect_remappings = false
5913                    "#,
5914            )?;
5915
5916            jail.create_file(
5917                "foundry.toml",
5918                r#"
5919                    [profile.default]
5920                    extends = "base.toml"
5921                    remappings = [
5922                        "@custom/=lib/custom/",
5923                        "ds-test/=lib/forge-std/lib/ds-test/src/"  # Note: This will be added alongside base remappings
5924                    ]
5925                    "#,
5926            )?;
5927
5928            let config = Config::load().unwrap();
5929
5930            // Remappings array is now concatenated with admerge (base + local)
5931            assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
5932            assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
5933            assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
5934            assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
5935
5936            // auto_detect_remappings should be inherited
5937            assert!(!config.auto_detect_remappings);
5938
5939            Ok(())
5940        });
5941    }
5942
5943    #[test]
5944    fn test_inheritance_with_multiple_profiles_and_single_file() {
5945        figment::Jail::expect_with(|jail| {
5946            // Create base config with prod and test profiles
5947            jail.create_file(
5948                "base.toml",
5949                r#"
5950                    [profile.prod]
5951                    optimizer = true
5952                    optimizer_runs = 10000
5953                    via_ir = true
5954
5955                    [profile.test]
5956                    optimizer = false
5957
5958                    [profile.test.fuzz]
5959                    runs = 100
5960                    "#,
5961            )?;
5962
5963            // Local config inherits from base for prod profile
5964            jail.create_file(
5965                "foundry.toml",
5966                r#"
5967                    [profile.prod]
5968                    extends = "base.toml"
5969                    evm_version = "shanghai"  # Additional setting
5970
5971                    [profile.test]
5972                    extends = "base.toml"
5973
5974                    [profile.test.fuzz]
5975                    runs = 500  # Override
5976                    "#,
5977            )?;
5978
5979            // Test prod profile
5980            jail.set_env("FOUNDRY_PROFILE", "prod");
5981            let config = Config::load().unwrap();
5982            assert_eq!(config.optimizer, Some(true));
5983            assert_eq!(config.optimizer_runs, Some(10000));
5984            assert_eq!(config.via_ir, true);
5985            assert_eq!(config.evm_version, EvmVersion::Shanghai);
5986
5987            // Test test profile
5988            jail.set_env("FOUNDRY_PROFILE", "test");
5989            let config = Config::load().unwrap();
5990            assert_eq!(config.optimizer, Some(false));
5991            assert_eq!(config.fuzz.runs, 500);
5992
5993            Ok(())
5994        });
5995    }
5996
5997    #[test]
5998    fn test_inheritance_with_multiple_profiles_and_files() {
5999        figment::Jail::expect_with(|jail| {
6000            jail.create_file(
6001                "prod.toml",
6002                r#"
6003                    [profile.prod]
6004                    optimizer = true
6005                    optimizer_runs = 20000
6006                    gas_limit = 50000000
6007                    "#,
6008            )?;
6009            jail.create_file(
6010                "dev.toml",
6011                r#"
6012                    [profile.dev]
6013                    optimizer = true
6014                    optimizer_runs = 333
6015                    gas_limit = 555555
6016                    "#,
6017            )?;
6018
6019            // Local config with only both profiles
6020            jail.create_file(
6021                "foundry.toml",
6022                r#"
6023                    [profile.dev]
6024                    extends = "dev.toml"
6025                    sender = "0x0000000000000000000000000000000000000001"
6026
6027                    [profile.prod]
6028                    extends = "prod.toml"
6029                    sender = "0x0000000000000000000000000000000000000002"
6030                    "#,
6031            )?;
6032
6033            // Test that prod profile correctly inherits even without a default profile
6034            jail.set_env("FOUNDRY_PROFILE", "dev");
6035            let config = Config::load().unwrap();
6036            assert_eq!(config.optimizer, Some(true));
6037            assert_eq!(config.optimizer_runs, Some(333));
6038            assert_eq!(config.gas_limit, 555555.into());
6039            assert_eq!(
6040                config.sender,
6041                "0x0000000000000000000000000000000000000001"
6042                    .parse::<alloy_primitives::Address>()
6043                    .unwrap()
6044            );
6045
6046            // Test that prod profile correctly inherits even without a default profile
6047            jail.set_env("FOUNDRY_PROFILE", "prod");
6048            let config = Config::load().unwrap();
6049            assert_eq!(config.optimizer, Some(true));
6050            assert_eq!(config.optimizer_runs, Some(20000));
6051            assert_eq!(config.gas_limit, 50000000.into());
6052            assert_eq!(
6053                config.sender,
6054                "0x0000000000000000000000000000000000000002"
6055                    .parse::<alloy_primitives::Address>()
6056                    .unwrap()
6057            );
6058
6059            Ok(())
6060        });
6061    }
6062
6063    #[test]
6064    fn test_extends_strategy_extend_arrays() {
6065        figment::Jail::expect_with(|jail| {
6066            // Create base config with arrays
6067            jail.create_file(
6068                "base.toml",
6069                r#"
6070                    [profile.default]
6071                    libs = ["lib", "node_modules"]
6072                    ignored_error_codes = [5667, 1878]
6073                    optimizer_runs = 200
6074                    "#,
6075            )?;
6076
6077            // Local config extends with extend-arrays strategy (concatenates arrays)
6078            jail.create_file(
6079                "foundry.toml",
6080                r#"
6081                    [profile.default]
6082                    extends = "base.toml"
6083                    libs = ["mylib", "customlib"]
6084                    ignored_error_codes = [1234]
6085                    optimizer_runs = 500
6086                    "#,
6087            )?;
6088
6089            let config = Config::load().unwrap();
6090
6091            // Arrays should be concatenated (base + local)
6092            assert_eq!(config.libs.len(), 4);
6093            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6094            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6095            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6096            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6097
6098            assert_eq!(config.ignored_error_codes.len(), 3);
6099            assert!(
6100                config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6101            ); // 5667
6102            assert!(
6103                config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
6104            ); // 1878
6105            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234 - generic
6106
6107            // Non-array values should be replaced
6108            assert_eq!(config.optimizer_runs, Some(500));
6109
6110            Ok(())
6111        });
6112    }
6113
6114    #[test]
6115    fn test_extends_strategy_replace_arrays() {
6116        figment::Jail::expect_with(|jail| {
6117            // Create base config with arrays
6118            jail.create_file(
6119                "base.toml",
6120                r#"
6121                    [profile.default]
6122                    libs = ["lib", "node_modules"]
6123                    ignored_error_codes = [5667, 1878]
6124                    optimizer_runs = 200
6125                    "#,
6126            )?;
6127
6128            // Local config extends with replace-arrays strategy (replaces arrays entirely)
6129            jail.create_file(
6130                "foundry.toml",
6131                r#"
6132                    [profile.default]
6133                    extends = { path = "base.toml", strategy = "replace-arrays" }
6134                    libs = ["mylib", "customlib"]
6135                    ignored_error_codes = [1234]
6136                    optimizer_runs = 500
6137                    "#,
6138            )?;
6139
6140            let config = Config::load().unwrap();
6141
6142            // Arrays should be replaced entirely (only local values)
6143            assert_eq!(config.libs.len(), 2);
6144            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6145            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6146            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6147            assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6148
6149            assert_eq!(config.ignored_error_codes.len(), 1);
6150            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234
6151            assert!(
6152                !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6153            ); // 5667
6154
6155            // Non-array values should be replaced
6156            assert_eq!(config.optimizer_runs, Some(500));
6157
6158            Ok(())
6159        });
6160    }
6161
6162    #[test]
6163    fn test_extends_strategy_no_collision_success() {
6164        figment::Jail::expect_with(|jail| {
6165            // Create base config
6166            jail.create_file(
6167                "base.toml",
6168                r#"
6169                    [profile.default]
6170                    optimizer = true
6171                    optimizer_runs = 200
6172                    src = "src"
6173                    "#,
6174            )?;
6175
6176            // Local config extends with no-collision strategy and no conflicts
6177            jail.create_file(
6178                "foundry.toml",
6179                r#"
6180                    [profile.default]
6181                    extends = { path = "base.toml", strategy = "no-collision" }
6182                    test = "tests"
6183                    libs = ["lib"]
6184                    "#,
6185            )?;
6186
6187            let config = Config::load().unwrap();
6188
6189            // Values from base should be present
6190            assert_eq!(config.optimizer, Some(true));
6191            assert_eq!(config.optimizer_runs, Some(200));
6192            assert_eq!(config.src, PathBuf::from("src"));
6193
6194            // Values from local should be present
6195            assert_eq!(config.test, PathBuf::from("tests"));
6196            assert_eq!(config.libs.len(), 1);
6197            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6198
6199            Ok(())
6200        });
6201    }
6202
6203    #[test]
6204    fn test_extends_strategy_no_collision_error() {
6205        figment::Jail::expect_with(|jail| {
6206            // Create base config
6207            jail.create_file(
6208                "base.toml",
6209                r#"
6210                    [profile.default]
6211                    optimizer = true
6212                    optimizer_runs = 200
6213                    libs = ["lib", "node_modules"]
6214                    "#,
6215            )?;
6216
6217            // Local config extends with no-collision strategy but has conflicts
6218            jail.create_file(
6219                "foundry.toml",
6220                r#"
6221                    [profile.default]
6222                    extends = { path = "base.toml", strategy = "no-collision" }
6223                    optimizer_runs = 500
6224                    libs = ["mylib"]
6225                    "#,
6226            )?;
6227
6228            // Loading should fail due to key collision
6229            let result = Config::load();
6230
6231            if let Ok(config) = result {
6232                panic!(
6233                    "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
6234                    config.optimizer_runs, config.libs
6235                );
6236            }
6237
6238            let err = result.unwrap_err();
6239            let err_str = err.to_string();
6240            assert!(
6241                err_str.contains("Key collision detected") || err_str.contains("collision"),
6242                "Error message doesn't mention collision: {err_str}"
6243            );
6244
6245            Ok(())
6246        });
6247    }
6248
6249    #[test]
6250    fn test_extends_both_syntaxes() {
6251        figment::Jail::expect_with(|jail| {
6252            // Create base config
6253            jail.create_file(
6254                "base.toml",
6255                r#"
6256                    [profile.default]
6257                    libs = ["lib"]
6258                    optimizer = true
6259                    "#,
6260            )?;
6261
6262            // Test 1: Simple string syntax (should use default extend-arrays)
6263            jail.create_file(
6264                "foundry_string.toml",
6265                r#"
6266                    [profile.default]
6267                    extends = "base.toml"
6268                    libs = ["custom"]
6269                    "#,
6270            )?;
6271
6272            // Test 2: Object syntax with explicit strategy
6273            jail.create_file(
6274                "foundry_object.toml",
6275                r#"
6276                    [profile.default]
6277                    extends = { path = "base.toml", strategy = "replace-arrays" }
6278                    libs = ["custom"]
6279                    "#,
6280            )?;
6281
6282            // Test string syntax (default extend-arrays)
6283            jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
6284            let config = Config::load().unwrap();
6285            assert_eq!(config.libs.len(), 2); // Should concatenate
6286            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6287            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6288
6289            // Test object syntax (replace-arrays)
6290            jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
6291            let config = Config::load().unwrap();
6292            assert_eq!(config.libs.len(), 1); // Should replace
6293            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6294            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6295
6296            Ok(())
6297        });
6298    }
6299
6300    #[test]
6301    fn test_extends_strategy_default_is_extend_arrays() {
6302        figment::Jail::expect_with(|jail| {
6303            // Create base config
6304            jail.create_file(
6305                "base.toml",
6306                r#"
6307                    [profile.default]
6308                    libs = ["lib", "node_modules"]
6309                    optimizer = true
6310                    "#,
6311            )?;
6312
6313            // Local config extends without specifying strategy (should default to extend-arrays)
6314            jail.create_file(
6315                "foundry.toml",
6316                r#"
6317                    [profile.default]
6318                    extends = "base.toml"
6319                    libs = ["custom"]
6320                    optimizer = false
6321                    "#,
6322            )?;
6323
6324            // Should work with default extend-arrays strategy
6325            let config = Config::load().unwrap();
6326
6327            // Arrays should be concatenated by default
6328            assert_eq!(config.libs.len(), 3);
6329            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6330            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6331            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6332
6333            // Non-array values should be replaced
6334            assert_eq!(config.optimizer, Some(false));
6335
6336            Ok(())
6337        });
6338    }
6339
6340    #[test]
6341    fn test_deprecated_deny_warnings_is_handled() {
6342        figment::Jail::expect_with(|jail| {
6343            jail.create_file(
6344                "foundry.toml",
6345                r#"
6346                [profile.default]
6347                deny_warnings = true
6348                "#,
6349            )?;
6350            let config = Config::load().unwrap();
6351
6352            // Assert that the deprecated flag is correctly interpreted
6353            assert_eq!(config.deny, DenyLevel::Warnings);
6354            Ok(())
6355        });
6356    }
6357
6358    #[test]
6359    fn warns_on_unknown_keys_in_profile() {
6360        figment::Jail::expect_with(|jail| {
6361            jail.create_file(
6362                "foundry.toml",
6363                r#"
6364                [profile.default]
6365                unknown_key_xyz = 123
6366                "#,
6367            )?;
6368
6369            let cfg = Config::load().unwrap();
6370            assert!(cfg.warnings.iter().any(
6371                |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "unknown_key_xyz")
6372            ));
6373            Ok(())
6374        });
6375    }
6376}