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