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