ethers_solc/
filter.rs

1//! Types to apply filter to input types
2
3use crate::{
4    artifacts::{output_selection::OutputSelection, Settings},
5    resolver::GraphEdges,
6    Source, Sources,
7};
8use std::{
9    collections::BTreeMap,
10    fmt,
11    fmt::Formatter,
12    path::{Path, PathBuf},
13};
14
15/// A predicate property that determines whether a file satisfies a certain condition
16pub trait FileFilter {
17    /// The predicate function that should return if the given `file` should be included.
18    fn is_match(&self, file: &Path) -> bool;
19}
20
21impl<F> FileFilter for F
22where
23    F: Fn(&Path) -> bool,
24{
25    fn is_match(&self, file: &Path) -> bool {
26        (self)(file)
27    }
28}
29
30/// An [FileFilter] that matches all solidity files that end with `.t.sol`
31#[derive(Default)]
32pub struct TestFileFilter {
33    _priv: (),
34}
35
36impl fmt::Debug for TestFileFilter {
37    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
38        f.debug_struct("TestFileFilter").finish()
39    }
40}
41
42impl fmt::Display for TestFileFilter {
43    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44        f.write_str("TestFileFilter")
45    }
46}
47
48impl FileFilter for TestFileFilter {
49    fn is_match(&self, file: &Path) -> bool {
50        file.file_name().and_then(|s| s.to_str()).map(|s| s.ends_with(".t.sol")).unwrap_or_default()
51    }
52}
53
54/// A type that can apply a filter to a set of preprocessed [FilteredSources] in order to set sparse
55/// output for specific files
56#[derive(Default)]
57pub enum SparseOutputFilter {
58    /// Sets the configured [OutputSelection] for dirty files only.
59    ///
60    /// In other words, we request the output of solc only for files that have been detected as
61    /// _dirty_.
62    #[default]
63    AllDirty,
64    /// Apply an additional filter to [FilteredSources] to
65    Custom(Box<dyn FileFilter>),
66}
67
68impl SparseOutputFilter {
69    /// While solc needs all the files to compile the actual _dirty_ files, we can tell solc to
70    /// output everything for those dirty files as currently configured in the settings, but output
71    /// nothing for the other files that are _not_ dirty.
72    ///
73    /// This will modify the [OutputSelection] of the [Settings] so that we explicitly select the
74    /// files' output based on their state.
75    ///
76    /// This also takes the project's graph as input, this allows us to check if the files the
77    /// filter matches depend on libraries that need to be linked
78    pub fn sparse_sources(
79        &self,
80        sources: FilteredSources,
81        settings: &mut Settings,
82        graph: &GraphEdges,
83    ) -> Sources {
84        match self {
85            SparseOutputFilter::AllDirty => {
86                if !sources.all_dirty() {
87                    Self::all_dirty(&sources, settings)
88                }
89            }
90            SparseOutputFilter::Custom(f) => {
91                Self::apply_custom_filter(&sources, settings, graph, f)
92            }
93        };
94        sources.into()
95    }
96
97    /// applies a custom filter and prunes the output of those source files for which the filter
98    /// returns `false`.
99    ///
100    /// However, this could in accidentally pruning required link references (imported libraries)
101    /// that will be required at runtime. For example if the filter only matches test files
102    /// `*.t.sol` files and a test file makes use of a library that won't be inlined, then the
103    /// libraries bytecode will be missing. Therefore, we detect all linkReferences of a file
104    /// and treat them as if the filter would also apply to those.
105    #[allow(clippy::borrowed_box)]
106    fn apply_custom_filter(
107        sources: &FilteredSources,
108        settings: &mut Settings,
109        graph: &GraphEdges,
110        f: &Box<dyn FileFilter>,
111    ) {
112        tracing::trace!("optimizing output selection with custom filter",);
113        let selection = settings
114            .output_selection
115            .as_mut()
116            .remove("*")
117            .unwrap_or_else(OutputSelection::default_file_output_selection);
118
119        for (file, source) in sources.0.iter() {
120            let key = format!("{}", file.display());
121            if source.is_dirty() && f.is_match(file) {
122                settings.output_selection.as_mut().insert(key, selection.clone());
123
124                // the filter might not cover link references that will be required by the file, so
125                // we check if the file has any libraries that won't be inlined and include them as
126                // well
127                for link in graph.get_link_references(file) {
128                    settings
129                        .output_selection
130                        .as_mut()
131                        .insert(format!("{}", link.display()), selection.clone());
132                }
133            } else if !settings.output_selection.as_ref().contains_key(&key) {
134                tracing::trace!("using pruned output selection for {}", file.display());
135                settings
136                    .output_selection
137                    .as_mut()
138                    .insert(key, OutputSelection::empty_file_output_select());
139            }
140        }
141    }
142
143    /// prunes all clean sources and only selects an output for dirty sources
144    fn all_dirty(sources: &FilteredSources, settings: &mut Settings) {
145        // settings can be optimized
146        tracing::trace!(
147            "optimizing output selection for {}/{} sources",
148            sources.clean().count(),
149            sources.len()
150        );
151
152        let selection = settings
153            .output_selection
154            .as_mut()
155            .remove("*")
156            .unwrap_or_else(OutputSelection::default_file_output_selection);
157
158        for (file, source) in sources.0.iter() {
159            if source.is_dirty() {
160                settings
161                    .output_selection
162                    .as_mut()
163                    .insert(format!("{}", file.display()), selection.clone());
164            } else {
165                tracing::trace!("using pruned output selection for {}", file.display());
166                settings.output_selection.as_mut().insert(
167                    format!("{}", file.display()),
168                    OutputSelection::empty_file_output_select(),
169                );
170            }
171        }
172    }
173}
174
175impl From<Box<dyn FileFilter>> for SparseOutputFilter {
176    fn from(f: Box<dyn FileFilter>) -> Self {
177        SparseOutputFilter::Custom(f)
178    }
179}
180
181impl fmt::Debug for SparseOutputFilter {
182    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
183        match self {
184            SparseOutputFilter::AllDirty => f.write_str("AllDirty"),
185            SparseOutputFilter::Custom(_) => f.write_str("Custom"),
186        }
187    }
188}
189
190/// Container type for a set of [FilteredSource]
191#[derive(Debug, Clone, Eq, PartialEq)]
192pub struct FilteredSources(pub BTreeMap<PathBuf, FilteredSource>);
193
194impl FilteredSources {
195    pub fn is_empty(&self) -> bool {
196        self.0.is_empty()
197    }
198
199    pub fn len(&self) -> usize {
200        self.0.len()
201    }
202
203    /// Returns `true` if all files are dirty
204    pub fn all_dirty(&self) -> bool {
205        self.0.values().all(|s| s.is_dirty())
206    }
207
208    /// Returns all entries that are dirty
209    pub fn dirty(&self) -> impl Iterator<Item = (&PathBuf, &FilteredSource)> + '_ {
210        self.0.iter().filter(|(_, s)| s.is_dirty())
211    }
212
213    /// Returns all entries that are clean
214    pub fn clean(&self) -> impl Iterator<Item = (&PathBuf, &FilteredSource)> + '_ {
215        self.0.iter().filter(|(_, s)| !s.is_dirty())
216    }
217
218    /// Returns all dirty files
219    pub fn dirty_files(&self) -> impl Iterator<Item = &PathBuf> + fmt::Debug + '_ {
220        self.0.iter().filter_map(|(k, s)| s.is_dirty().then_some(k))
221    }
222}
223
224impl From<FilteredSources> for Sources {
225    fn from(sources: FilteredSources) -> Self {
226        sources.0.into_iter().map(|(k, v)| (k, v.into_source())).collect()
227    }
228}
229
230impl From<Sources> for FilteredSources {
231    fn from(s: Sources) -> Self {
232        FilteredSources(s.into_iter().map(|(key, val)| (key, FilteredSource::Dirty(val))).collect())
233    }
234}
235
236impl From<BTreeMap<PathBuf, FilteredSource>> for FilteredSources {
237    fn from(s: BTreeMap<PathBuf, FilteredSource>) -> Self {
238        FilteredSources(s)
239    }
240}
241
242impl AsRef<BTreeMap<PathBuf, FilteredSource>> for FilteredSources {
243    fn as_ref(&self) -> &BTreeMap<PathBuf, FilteredSource> {
244        &self.0
245    }
246}
247
248impl AsMut<BTreeMap<PathBuf, FilteredSource>> for FilteredSources {
249    fn as_mut(&mut self) -> &mut BTreeMap<PathBuf, FilteredSource> {
250        &mut self.0
251    }
252}
253
254/// Represents the state of a filtered [Source]
255#[derive(Debug, Clone, Eq, PartialEq)]
256pub enum FilteredSource {
257    /// A source that fits the _dirty_ criteria
258    Dirty(Source),
259    /// A source that does _not_ fit the _dirty_ criteria but is included in the filtered set
260    /// because a _dirty_ file pulls it in, either directly on indirectly.
261    Clean(Source),
262}
263
264impl FilteredSource {
265    /// Returns the underlying source
266    pub fn source(&self) -> &Source {
267        match self {
268            FilteredSource::Dirty(s) => s,
269            FilteredSource::Clean(s) => s,
270        }
271    }
272
273    /// Consumes the type and returns the underlying source
274    pub fn into_source(self) -> Source {
275        match self {
276            FilteredSource::Dirty(s) => s,
277            FilteredSource::Clean(s) => s,
278        }
279    }
280
281    /// Whether this file is actually dirt
282    pub fn is_dirty(&self) -> bool {
283        matches!(self, FilteredSource::Dirty(_))
284    }
285}
286
287/// Helper type that determines the state of a source file
288#[derive(Debug)]
289pub struct FilteredSourceInfo {
290    /// Path to the source file.
291    pub file: PathBuf,
292
293    /// Contents of the file.
294    pub source: Source,
295
296    /// Index in the [GraphEdges].
297    pub idx: usize,
298
299    /// Whether this file is actually dirty.
300    ///
301    /// See also `ArtifactsCacheInner::is_dirty`
302    pub dirty: bool,
303}