Skip to main content

harper_core/expr/
mod.rs

1//! An `Expr` is a declarative way to express whether a certain set of tokens fulfill a criteria.
2//!
3//! For example, if we want to look for the word "that" followed by an adjective, we could build an
4//! expression to do so.
5//!
6//! The actual searching is done by another system (usually a part of the [lint framework](crate::linting::ExprLinter)).
7//! It iterates through a document, checking if each index matches the criteria.
8//!
9//! When supplied a specific position in a token stream, the technical job of an `Expr` is to determine the window of tokens (including the cursor itself) that fulfills whatever criteria the author desires.
10//!
11//! The goal of the `Expr` initiative is to make rules easier to _read_ as well as to write.
12//! Gone are the days of trying to manually parse the logic of another man's Rust code.
13//!
14//! See also: [`SequenceExpr`].
15
16mod all;
17mod anchor_end;
18mod anchor_start;
19mod duration_expr;
20mod expr_map;
21mod filter;
22mod first_match_of;
23mod fixed_phrase;
24mod longest_match_of;
25mod mergeable_words;
26mod not;
27mod optional;
28mod pronoun_be;
29mod reflexive_pronoun;
30mod repeating;
31mod sequence_expr;
32mod similar_to_phrase;
33mod space_or_hyphen;
34mod spelled_number_expr;
35mod step;
36mod time_unit_expr;
37mod unless_step;
38mod word_expr_group;
39
40#[cfg(not(feature = "concurrent"))]
41use std::rc::Rc;
42use std::sync::Arc;
43
44pub use all::All;
45pub use anchor_end::AnchorEnd;
46pub use anchor_start::AnchorStart;
47pub use duration_expr::DurationExpr;
48pub use expr_map::ExprMap;
49pub use filter::Filter;
50pub use first_match_of::FirstMatchOf;
51pub use fixed_phrase::FixedPhrase;
52pub use longest_match_of::LongestMatchOf;
53pub use mergeable_words::MergeableWords;
54pub use not::Not;
55pub use optional::Optional;
56pub use pronoun_be::PronounBe;
57pub use reflexive_pronoun::ReflexivePronoun;
58pub use repeating::Repeating;
59pub use sequence_expr::SequenceExpr;
60pub use similar_to_phrase::SimilarToPhrase;
61pub use space_or_hyphen::SpaceOrHyphen;
62pub use spelled_number_expr::SpelledNumberExpr;
63pub use step::Step;
64pub use time_unit_expr::TimeUnitExpr;
65pub use unless_step::UnlessStep;
66pub use word_expr_group::WordExprGroup;
67
68use crate::{Document, LSend, Span, Token};
69
70pub trait Expr: LSend {
71    fn run(&self, cursor: usize, tokens: &[Token], source: &[char]) -> Option<Span<Token>>;
72}
73
74impl<S> Expr for S
75where
76    S: Step + ?Sized,
77{
78    fn run(&self, cursor: usize, tokens: &[Token], source: &[char]) -> Option<Span<Token>> {
79        self.step(tokens, cursor, source).map(|s| {
80            if s >= 0 {
81                Span::new_with_len(cursor, s as usize)
82            } else {
83                Span::new(add(cursor, s).unwrap(), cursor)
84            }
85        })
86    }
87}
88
89impl<E> Expr for Arc<E>
90where
91    E: Expr,
92{
93    fn run(&self, cursor: usize, tokens: &[Token], source: &[char]) -> Option<Span<Token>> {
94        self.as_ref().run(cursor, tokens, source)
95    }
96}
97
98impl Expr for Box<dyn Expr> {
99    fn run(&self, cursor: usize, tokens: &[Token], source: &[char]) -> Option<Span<Token>> {
100        self.as_ref().run(cursor, tokens, source)
101    }
102}
103
104#[cfg(not(feature = "concurrent"))]
105impl<E> Expr for Rc<E>
106where
107    E: Expr,
108{
109    fn run(&self, cursor: usize, tokens: &[Token], source: &[char]) -> Option<Span<Token>> {
110        self.as_ref().run(cursor, tokens, source)
111    }
112}
113
114fn add(u: usize, i: isize) -> Option<usize> {
115    if i.is_negative() {
116        u.checked_sub(i.wrapping_abs() as u32 as usize)
117    } else {
118        u.checked_add(i as usize)
119    }
120}
121
122pub trait ExprExt {
123    /// Iterate over all matches of this expression in the document, automatically filtering out
124    /// overlapping matches, preferring the first.
125    fn iter_matches<'a>(
126        &'a self,
127        tokens: &'a [Token],
128        source: &'a [char],
129    ) -> Box<dyn Iterator<Item = Span<Token>> + 'a>;
130
131    fn iter_matches_in_doc<'a>(
132        &'a self,
133        doc: &'a Document,
134    ) -> Box<dyn Iterator<Item = Span<Token>> + 'a>;
135}
136
137impl<E: ?Sized> ExprExt for E
138where
139    E: Expr,
140{
141    fn iter_matches<'a>(
142        &'a self,
143        tokens: &'a [Token],
144        source: &'a [char],
145    ) -> Box<dyn Iterator<Item = Span<Token>> + 'a> {
146        let mut last_end = 0usize;
147
148        Box::new((0..tokens.len()).filter_map(move |i| {
149            let span = self.run(i, tokens, source)?;
150            if span.start >= last_end {
151                last_end = span.end;
152                Some(span)
153            } else {
154                None
155            }
156        }))
157    }
158
159    fn iter_matches_in_doc<'a>(
160        &'a self,
161        doc: &'a Document,
162    ) -> Box<dyn Iterator<Item = Span<Token>> + 'a> {
163        Box::new(self.iter_matches(doc.get_tokens(), doc.get_source()))
164    }
165}
166
167pub trait OwnedExprExt {
168    fn or(self, other: impl Expr + 'static) -> FirstMatchOf;
169    fn and(self, other: impl Expr + 'static) -> All;
170    fn but_not(self, other: impl Expr + 'static) -> All;
171    fn or_longest(self, other: impl Expr + 'static) -> LongestMatchOf;
172}
173
174impl<E> OwnedExprExt for E
175where
176    E: Expr + 'static,
177{
178    /// Returns an expression that matches either the current one or the expression contained in `other`.
179    fn or(self, other: impl Expr + 'static) -> FirstMatchOf {
180        let exprs: Vec<Box<dyn Expr>> = vec![Box::new(self), Box::new(other)];
181        FirstMatchOf::new(exprs)
182    }
183
184    /// Returns an expression that matches only if both the current one and the expression contained in `other` do.
185    fn and(self, other: impl Expr + 'static) -> All {
186        let exprs: Vec<Box<dyn Expr>> = vec![Box::new(self), Box::new(other)];
187        All::new(exprs)
188    }
189
190    /// Returns an expression that matches only if the current one matches and the expression contained in `other` does not.
191    fn but_not(self, other: impl Expr + 'static) -> All {
192        self.and(UnlessStep::new(other, |_tok: &Token, _src: &[char]| true))
193    }
194
195    /// Returns an expression that matches the longest of the current one or the expression contained in `other`.
196    ///
197    /// If you don't need the longest match, prefer using the short-circuiting [`Self::or()`] instead.
198    fn or_longest(self, other: impl Expr + 'static) -> LongestMatchOf {
199        let exprs: Vec<Box<dyn Expr>> = vec![Box::new(self), Box::new(other)];
200        LongestMatchOf::new(exprs)
201    }
202}
203
204pub trait IntoBoxedExpr {
205    fn into_boxed(self) -> Box<dyn Expr>;
206}
207
208impl<T: Expr + 'static> IntoBoxedExpr for Box<T> {
209    fn into_boxed(self) -> Box<dyn Expr> {
210        self
211    }
212}
213
214impl IntoBoxedExpr for Box<dyn Expr> {
215    fn into_boxed(self) -> Box<dyn Expr> {
216        self
217    }
218}
219
220pub trait AsBoxedExpr {
221    fn into_boxed_expr(self) -> Box<dyn Expr>;
222}
223
224impl<T: Expr + 'static> AsBoxedExpr for Box<T> {
225    fn into_boxed_expr(self) -> Box<dyn Expr> {
226        self
227    }
228}
229
230impl AsBoxedExpr for Box<dyn Expr> {
231    fn into_boxed_expr(self) -> Box<dyn Expr> {
232        self
233    }
234}