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