gix_attributes/search/
outcome.rs

1use bstr::{BString, ByteSlice};
2use gix_glob::Pattern;
3use kstring::{KString, KStringRef};
4
5use crate::{
6    search::{
7        refmap::RefMapKey, Assignments, AttributeId, Attributes, MatchKind, Metadata, MetadataCollection, Outcome,
8        TrackedAssignment, Value,
9    },
10    AssignmentRef, NameRef, StateRef,
11};
12
13/// Initialization
14impl Outcome {
15    /// Initialize this instance to collect outcomes for all names in `collection`, which represents all possible attributes
16    /// or macros we may visit, and [`reset`][Self::reset()] it unconditionally.
17    ///
18    /// This must be called after each time `collection` changes.
19    pub fn initialize(&mut self, collection: &MetadataCollection) {
20        if self.matches_by_id.len() != collection.name_to_meta.len() {
21            let global_num_attrs = collection.name_to_meta.len();
22
23            self.matches_by_id.resize(global_num_attrs, Default::default());
24
25            // NOTE: This works only under the assumption that macros remain defined.
26            for (order, macro_attributes) in collection.iter().filter_map(|(_, meta)| {
27                (!meta.macro_attributes.is_empty()).then_some((meta.id.0, &meta.macro_attributes))
28            }) {
29                self.matches_by_id[order].macro_attributes.clone_from(macro_attributes);
30            }
31
32            for (name, id) in self.selected.iter_mut().filter(|(_, id)| id.is_none()) {
33                *id = collection.name_to_meta.get(name.as_str()).map(|meta| meta.id);
34            }
35        }
36        self.reset();
37    }
38
39    /// Like [`initialize()`][Self::initialize()], but limits the set of attributes to look for and fill in
40    /// to `attribute_names`.
41    /// Users of this instance should prefer to limit their search as this would allow it to finish earlier.
42    ///
43    /// Note that `attribute_names` aren't validated to be valid names here, as invalid names definitely will always be unspecified.
44    pub fn initialize_with_selection<'a>(
45        &mut self,
46        collection: &MetadataCollection,
47        attribute_names: impl IntoIterator<Item = impl Into<KStringRef<'a>>>,
48    ) {
49        self.initialize_with_selection_inner(collection, &mut attribute_names.into_iter().map(Into::into));
50    }
51
52    fn initialize_with_selection_inner(
53        &mut self,
54        collection: &MetadataCollection,
55        attribute_names: &mut dyn Iterator<Item = KStringRef<'_>>,
56    ) {
57        self.selected.clear();
58        self.selected.extend(attribute_names.map(|name| {
59            (
60                name.to_owned(),
61                collection.name_to_meta.get(name.as_str()).map(|meta| meta.id),
62            )
63        }));
64
65        self.initialize(collection);
66        self.reset_remaining();
67    }
68
69    /// Prepare for a new search over the known set of attributes by resetting our state.
70    pub fn reset(&mut self) {
71        self.matches_by_id.iter_mut().for_each(|item| item.r#match = None);
72        self.attrs_stack.clear();
73        self.reset_remaining();
74    }
75
76    fn reset_remaining(&mut self) {
77        self.remaining = Some(if self.selected.is_empty() {
78            self.matches_by_id.len()
79        } else {
80            self.selected.iter().filter(|(_name, id)| id.is_some()).count()
81        });
82    }
83
84    /// A performance optimization which allows results from this instance to be efficiently copied over to `dest`.
85    /// For this to work, `collection` must be the one used to initialize our state, and `dest` should not have been initialized
86    /// with any meaningful collection initially, i.e. be empty the first time this method is called.
87    ///
88    /// Note that it's safe to call it multiple times, so that it can be called after this instance was used to store a search result.
89    pub fn copy_into(&self, collection: &MetadataCollection, dest: &mut Self) {
90        dest.initialize(collection);
91        dest.matches_by_id.clone_from(&self.matches_by_id);
92        if dest.patterns.len() != self.patterns.len() {
93            dest.patterns = self.patterns.clone();
94        }
95        if dest.assignments.len() != self.assignments.len() {
96            dest.assignments = self.assignments.clone();
97        }
98        if dest.source_paths.len() != self.source_paths.len() {
99            dest.source_paths = self.source_paths.clone();
100        }
101        dest.remaining = self.remaining;
102    }
103}
104
105/// Access
106impl Outcome {
107    /// Return an iterator over all filled attributes we were initialized with.
108    ///
109    /// ### Note
110    ///
111    /// If [`initialize_with_selection`][Self::initialize_with_selection()] was used,
112    /// use [`iter_selected()`][Self::iter_selected()] instead.
113    ///
114    /// ### Deviation
115    ///
116    /// It's possible that the order in which the attribute are returned (if not limited to a set of attributes) isn't exactly
117    /// the same as what `git` provides.
118    /// Ours is in order of declaration, whereas `git` seems to list macros first somehow. Since the values are the same, this
119    /// shouldn't be an issue.
120    pub fn iter(&self) -> impl Iterator<Item = crate::search::Match<'_>> {
121        self.matches_by_id
122            .iter()
123            .filter_map(|item| item.r#match.as_ref().map(|m| m.to_outer(self)))
124    }
125
126    /// Iterate over all matches of the attribute selection in their original order.
127    ///
128    /// This only yields values if this instance was initialized with [`Outcome::initialize_with_selection()`].
129    pub fn iter_selected(&self) -> impl Iterator<Item = crate::search::Match<'_>> {
130        static DUMMY: Pattern = Pattern {
131            text: BString::new(Vec::new()),
132            mode: gix_glob::pattern::Mode::empty(),
133            first_wildcard_pos: None,
134        };
135        self.selected.iter().map(|(name, id)| {
136            id.and_then(|id| self.matches_by_id[id.0].r#match.as_ref().map(|m| m.to_outer(self)))
137                .unwrap_or_else(|| crate::search::Match {
138                    pattern: &DUMMY,
139                    assignment: AssignmentRef {
140                        name: NameRef::try_from(name.as_bytes().as_bstr())
141                            .unwrap_or_else(|_| NameRef("invalid".into())),
142                        state: StateRef::Unspecified,
143                    },
144                    kind: MatchKind::Attribute { macro_id: None },
145                    location: crate::search::MatchLocation {
146                        source: None,
147                        sequence_number: 0,
148                    },
149                })
150        })
151    }
152
153    /// Obtain a match by the order of its attribute, if the order exists in our initialized attribute list and there was a match.
154    pub fn match_by_id(&self, id: AttributeId) -> Option<crate::search::Match<'_>> {
155        self.matches_by_id
156            .get(id.0)
157            .and_then(|m| m.r#match.as_ref().map(|m| m.to_outer(self)))
158    }
159
160    /// Return `true` if there is nothing more to be done as all attributes were filled.
161    pub fn is_done(&self) -> bool {
162        self.remaining() == 0
163    }
164}
165
166/// Mutation
167impl Outcome {
168    /// Fill all `attrs` and resolve them recursively if they are macros. Return `true` if there is no attribute left to be resolved and
169    /// we are totally done.
170    /// `pattern` is what matched a patch and is passed for contextual information,
171    /// providing `sequence_number` and `source` as well.
172    pub(crate) fn fill_attributes<'a>(
173        &mut self,
174        attrs: impl Iterator<Item = &'a TrackedAssignment>,
175        pattern: &gix_glob::Pattern,
176        source: Option<&std::path::PathBuf>,
177        sequence_number: usize,
178    ) -> bool {
179        self.attrs_stack.extend(
180            attrs
181                .filter(|attr| self.matches_by_id[attr.id.0].r#match.is_none())
182                .map(|attr| (attr.id, attr.inner.clone(), None)),
183        );
184        while let Some((id, assignment, parent_order)) = self.attrs_stack.pop() {
185            let slot = &mut self.matches_by_id[id.0];
186            if slot.r#match.is_some() {
187                continue;
188            }
189            // Let's be explicit - this is only non-empty for macros.
190            let is_macro = !slot.macro_attributes.is_empty();
191
192            slot.r#match = Some(Match {
193                pattern: self.patterns.insert(pattern),
194                assignment: self.assignments.insert_owned(assignment),
195                kind: if is_macro {
196                    MatchKind::Macro {
197                        parent_macro_id: parent_order,
198                    }
199                } else {
200                    MatchKind::Attribute { macro_id: parent_order }
201                },
202                location: MatchLocation {
203                    source: source.map(|path| self.source_paths.insert(path)),
204                    sequence_number,
205                },
206            });
207            if self.reduce_and_check_if_done(id) {
208                return true;
209            }
210
211            if is_macro {
212                // TODO(borrowchk): one fine day we should be able to re-borrow `slot` without having to redo the array access.
213                let slot = &self.matches_by_id[id.0];
214                self.attrs_stack.extend(
215                    slot.macro_attributes
216                        .iter()
217                        .filter(|attr| self.matches_by_id[attr.id.0].r#match.is_none())
218                        .map(|attr| (attr.id, attr.inner.clone(), Some(id))),
219                );
220            }
221        }
222        false
223    }
224}
225
226impl Outcome {
227    /// Given a list of `attrs` by order, return true if at least one of them is not set
228    pub(crate) fn has_unspecified_attributes(&self, mut attrs: impl Iterator<Item = AttributeId>) -> bool {
229        attrs.any(|order| self.matches_by_id[order.0].r#match.is_none())
230    }
231    /// Return the amount of attributes haven't yet been found.
232    ///
233    /// If this number reaches 0, then the search can be stopped as there is nothing more to fill in.
234    pub(crate) fn remaining(&self) -> usize {
235        self.remaining
236            .expect("BUG: instance must be initialized for each search set")
237    }
238
239    fn reduce_and_check_if_done(&mut self, attr: AttributeId) -> bool {
240        if self.selected.is_empty() || self.selected.iter().any(|(_name, id)| *id == Some(attr)) {
241            *self.remaining.as_mut().expect("initialized") -= 1;
242        }
243        self.is_done()
244    }
245}
246
247impl std::fmt::Debug for Outcome {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        struct AsDisplay<'a>(&'a dyn std::fmt::Display);
250        impl std::fmt::Debug for AsDisplay<'_> {
251            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252                self.0.fmt(f)
253            }
254        }
255
256        let mut dbg = f.debug_tuple("Outcome");
257        if self.selected.is_empty() {
258            for match_ in self.iter() {
259                dbg.field(&AsDisplay(&match_.assignment));
260            }
261        } else {
262            for match_ in self.iter_selected() {
263                dbg.field(&AsDisplay(&match_.assignment));
264            }
265        }
266        dbg.finish()
267    }
268}
269
270/// Mutation
271impl MetadataCollection {
272    /// Assign order ids to each attribute either in macros (along with macros themselves) or attributes of patterns, and store
273    /// them in this collection.
274    ///
275    /// Must be called before querying matches.
276    pub fn update_from_list(&mut self, list: &mut gix_glob::search::pattern::List<Attributes>) {
277        for pattern in &mut list.patterns {
278            match &mut pattern.value {
279                Value::MacroAssignments { id: order, assignments } => {
280                    *order = self.id_for_macro(
281                        pattern
282                            .pattern
283                            .text
284                            .to_str()
285                            .expect("valid macro names are always UTF8 and this was verified"),
286                        assignments,
287                    );
288                }
289                Value::Assignments(assignments) => {
290                    self.assign_order_to_attributes(assignments);
291                }
292            }
293        }
294    }
295}
296
297/// Access
298impl MetadataCollection {
299    /// Return an iterator over the contents of the map in an easy-to-consume form.
300    pub fn iter(&self) -> impl Iterator<Item = (&str, &Metadata)> {
301        self.name_to_meta.iter().map(|(k, v)| (k.as_str(), v))
302    }
303}
304
305impl MetadataCollection {
306    pub(crate) fn id_for_macro(&mut self, name: &str, attrs: &mut Assignments) -> AttributeId {
307        let order = match self.name_to_meta.get_mut(name) {
308            Some(meta) => meta.id,
309            None => {
310                let order = AttributeId(self.name_to_meta.len());
311                self.name_to_meta.insert(
312                    KString::from_ref(name),
313                    Metadata {
314                        id: order,
315                        macro_attributes: Default::default(),
316                    },
317                );
318                order
319            }
320        };
321
322        self.assign_order_to_attributes(attrs);
323        self.name_to_meta
324            .get_mut(name)
325            .expect("just added")
326            .macro_attributes
327            .clone_from(attrs);
328
329        order
330    }
331    pub(crate) fn id_for_attribute(&mut self, name: &str) -> AttributeId {
332        match self.name_to_meta.get(name) {
333            Some(meta) => meta.id,
334            None => {
335                let order = AttributeId(self.name_to_meta.len());
336                self.name_to_meta.insert(KString::from_ref(name), order.into());
337                order
338            }
339        }
340    }
341    pub(crate) fn assign_order_to_attributes(&mut self, attributes: &mut [TrackedAssignment]) {
342        for TrackedAssignment {
343            id: order,
344            inner: crate::Assignment { name, .. },
345        } in attributes
346        {
347            *order = self.id_for_attribute(&name.0);
348        }
349    }
350}
351
352impl From<AttributeId> for Metadata {
353    fn from(order: AttributeId) -> Self {
354        Metadata {
355            id: order,
356            macro_attributes: Default::default(),
357        }
358    }
359}
360
361impl MatchKind {
362    /// return the id of the macro that resolved us, or `None` if that didn't happen.
363    pub fn source_id(&self) -> Option<AttributeId> {
364        match self {
365            MatchKind::Attribute { macro_id: id } | MatchKind::Macro { parent_macro_id: id } => *id,
366        }
367    }
368}
369
370/// A version of `Match` without references.
371#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
372pub struct Match {
373    /// The glob pattern itself, like `/target/*`.
374    pub pattern: RefMapKey,
375    /// The key=value pair of the attribute that matched at the pattern. There can be multiple matches per pattern.
376    pub assignment: RefMapKey,
377    /// Additional information about the kind of match.
378    pub kind: MatchKind,
379    /// Information about the location of the match.
380    pub location: MatchLocation,
381}
382
383impl Match {
384    fn to_outer<'a>(&self, out: &'a Outcome) -> crate::search::Match<'a> {
385        crate::search::Match {
386            pattern: out.patterns.resolve(self.pattern).expect("pattern still present"),
387            assignment: out
388                .assignments
389                .resolve(self.assignment)
390                .expect("assignment present")
391                .as_ref(),
392            kind: self.kind,
393            location: self.location.to_outer(out),
394        }
395    }
396}
397
398/// A version of `MatchLocation` without references.
399#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
400pub struct MatchLocation {
401    /// The path to the source from which the pattern was loaded, or `None` if it was specified by other means.
402    pub source: Option<RefMapKey>,
403    /// The line at which the pattern was found in its `source` file, or the occurrence in which it was provided.
404    pub sequence_number: usize,
405}
406
407impl MatchLocation {
408    fn to_outer<'a>(&self, out: &'a Outcome) -> crate::search::MatchLocation<'a> {
409        crate::search::MatchLocation {
410            source: self
411                .source
412                .and_then(|source| out.source_paths.resolve(source).map(AsRef::as_ref)),
413            sequence_number: self.sequence_number,
414        }
415    }
416}