foundry_config/inline/
mod.rs

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