Skip to main content

foundry_config/inline/
mod.rs

1use std::collections::BTreeSet;
2
3use crate::Config;
4use alloy_primitives::map::HashMap;
5use figment::{
6    Figment, Profile, Provider,
7    value::{Dict, Map, Value},
8};
9use foundry_compilers::ProjectCompileOutput;
10use foundry_evm_networks::NetworkVariant;
11use itertools::Itertools;
12
13mod natspec;
14pub use natspec::*;
15
16const INLINE_CONFIG_PREFIX: &str = "forge-config:";
17
18type DataMap = Map<Profile, Dict>;
19
20/// Errors returned when parsing inline config.
21#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
22pub enum InlineConfigErrorKind {
23    /// Failed to parse inline config as TOML.
24    #[error(transparent)]
25    Parse(#[from] toml::de::Error),
26    /// An invalid profile has been provided.
27    #[error("invalid profile `{0}`; valid profiles: {1}")]
28    InvalidProfile(String, String),
29}
30
31/// Wrapper error struct that catches config parsing errors, enriching them with context information
32/// reporting the misconfigured line.
33#[derive(Debug, thiserror::Error)]
34#[error("Inline config error at {location}: {kind}")]
35pub struct InlineConfigError {
36    /// The span of the error in the format:
37    /// `dir/TestContract.t.sol:FuzzContract:10:12:111`
38    pub location: String,
39    /// The inner error
40    pub kind: InlineConfigErrorKind,
41}
42
43/// Represents per-test configurations, declared inline
44/// as structured comments in Solidity test files. This allows
45/// to create configs directly bound to a solidity test.
46#[derive(Clone, Debug, Default)]
47pub struct InlineConfig {
48    /// Contract-level configuration.
49    contract_level: HashMap<String, DataMap>,
50    /// Function-level configuration.
51    fn_level: HashMap<(String, String), DataMap>,
52}
53
54impl InlineConfig {
55    /// Creates a new, empty [`InlineConfig`].
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Tries to create a new instance by detecting inline configurations from the project compile
61    /// output.
62    pub fn new_parsed(output: &ProjectCompileOutput, config: &Config) -> eyre::Result<Self> {
63        let natspecs: Vec<NatSpec> = NatSpec::parse(output, &config.root);
64        let profiles = &config.profiles;
65        let mut inline = Self::new();
66        for natspec in &natspecs {
67            inline.insert(natspec)?;
68            // Validate after parsing as TOML.
69            natspec.validate_profiles(profiles)?;
70        }
71        Ok(inline)
72    }
73
74    /// Inserts a new [`NatSpec`] into the [`InlineConfig`].
75    pub fn insert(&mut self, natspec: &NatSpec) -> Result<(), InlineConfigError> {
76        let map = if let Some(function) = &natspec.function {
77            self.fn_level.entry((natspec.contract.clone(), function.clone())).or_default()
78        } else {
79            self.contract_level.entry(natspec.contract.clone()).or_default()
80        };
81        let joined = natspec
82            .config_values()
83            .map(|s| {
84                // Replace `-` with `_` for backwards compatibility with the old parser.
85                if let Some(idx) = s.find('=') {
86                    s[..idx].replace('-', "_") + &s[idx..]
87                } else {
88                    s.to_string()
89                }
90            })
91            .format("\n")
92            .to_string();
93        let data = toml::from_str::<DataMap>(&joined).map_err(|e| InlineConfigError {
94            location: natspec.location_string(),
95            kind: InlineConfigErrorKind::Parse(e),
96        })?;
97        extend_data_map(map, &data);
98        Ok(())
99    }
100
101    /// Returns a [`figment::Provider`] for this [`InlineConfig`] at the given contract and function
102    /// level.
103    pub const fn provide<'a>(
104        &'a self,
105        contract: &'a str,
106        function: &'a str,
107    ) -> InlineConfigProvider<'a> {
108        InlineConfigProvider { inline: self, contract, function }
109    }
110
111    /// Merges the inline configuration at the given contract and function level with the provided
112    /// base configuration.
113    pub fn merge(&self, contract: &str, function: &str, base: &Config) -> Figment {
114        Figment::from(base).merge(self.provide(contract, function))
115    }
116
117    /// Returns `true` if a configuration is present at the given contract level.
118    pub fn contains_contract(&self, contract: &str) -> bool {
119        self.get_contract(contract).is_some_and(|map| !map.is_empty())
120    }
121
122    /// Returns `true` if a configuration is present at the function level.
123    ///
124    /// Does not include contract-level configurations.
125    pub fn contains_function(&self, contract: &str, function: &str) -> bool {
126        self.get_function(contract, function).is_some_and(|map| !map.is_empty())
127    }
128
129    /// Returns the configured [`NetworkVariant`] for a given test, checking function-level first
130    /// then contract-level. Returns `None` if no network annotation is present.
131    pub fn network_for(
132        &self,
133        profile: &Profile,
134        contract: &str,
135        function: &str,
136    ) -> Option<NetworkVariant> {
137        let data = self.provide(contract, function).data().ok()?;
138        let dict = data.get(profile).or_else(|| data.get(&Profile::Default))?;
139        if let Some(Value::Dict(_, networks)) = dict.get("networks")
140            && let Some(Value::String(_, s)) = networks.get("network")
141        {
142            return s.parse().ok();
143        }
144        None
145    }
146
147    /// Returns all distinct [`NetworkVariant`]s referenced in any inline config annotation.
148    ///
149    /// This is used to determine whether a multi-network test pass is needed.
150    pub fn referenced_override_networks(&self, profile: &Profile) -> Vec<NetworkVariant> {
151        let mut seen = BTreeSet::new();
152        for (contract, function) in self.fn_level.keys() {
153            if let Some(v) = self.network_for(profile, contract, function) {
154                seen.insert(v);
155            }
156        }
157        for contract in self.contract_level.keys() {
158            if let Some(v) = self.network_for(profile, contract, "") {
159                seen.insert(v);
160            }
161        }
162        seen.into_iter().collect()
163    }
164
165    fn get_contract(&self, contract: &str) -> Option<&DataMap> {
166        self.contract_level.get(contract)
167    }
168
169    fn get_function(&self, contract: &str, function: &str) -> Option<&DataMap> {
170        let key = (contract.to_string(), function.to_string());
171        self.fn_level.get(&key)
172    }
173}
174
175/// [`figment::Provider`] for [`InlineConfig`] at a given contract and function level.
176///
177/// Created by [`InlineConfig::provide`].
178#[derive(Clone, Debug)]
179pub struct InlineConfigProvider<'a> {
180    inline: &'a InlineConfig,
181    contract: &'a str,
182    function: &'a str,
183}
184
185impl Provider for InlineConfigProvider<'_> {
186    fn metadata(&self) -> figment::Metadata {
187        figment::Metadata::named("inline config")
188    }
189
190    fn data(&self) -> figment::Result<DataMap> {
191        let mut map = DataMap::new();
192        if let Some(new) = self.inline.get_contract(self.contract) {
193            extend_data_map(&mut map, new);
194        }
195        if let Some(new) = self.inline.get_function(self.contract, self.function) {
196            extend_data_map(&mut map, new);
197        }
198        Ok(map)
199    }
200}
201
202fn extend_data_map(map: &mut DataMap, new: &DataMap) {
203    for (profile, data) in new {
204        extend_dict(map.entry(profile.clone()).or_default(), data);
205    }
206}
207
208fn extend_dict(dict: &mut Dict, new: &Dict) {
209    for (k, v) in new {
210        match dict.entry(k.clone()) {
211            std::collections::btree_map::Entry::Vacant(entry) => {
212                entry.insert(v.clone());
213            }
214            std::collections::btree_map::Entry::Occupied(entry) => {
215                extend_value(entry.into_mut(), v);
216            }
217        }
218    }
219}
220
221fn extend_value(value: &mut Value, new: &Value) {
222    match (value, new) {
223        (Value::Dict(tag, dict), Value::Dict(new_tag, new_dict)) => {
224            *tag = *new_tag;
225            extend_dict(dict, new_dict);
226        }
227        (value, new) => *value = new.clone(),
228    }
229}