Skip to main content

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