litcheck_filecheck/pattern/matcher/matchers/
substring.rs

1use aho_corasick::{AhoCorasick, AhoCorasickBuilder, AhoCorasickKind, MatchKind, StartKind};
2
3use crate::common::*;
4
5/// This matcher searches for a given substring in the input buffer,
6/// with some control over how the search is conducted.
7pub struct SubstringMatcher<'a> {
8    /// The span of the pattern in the check file
9    /// from which this matcher is derived
10    span: SourceSpan,
11    /// The original pattern used to construct this matcher
12    pattern: Span<Cow<'a, str>>,
13    /// The automaton used to perform the search
14    searcher: AhoCorasick,
15}
16impl<'a> fmt::Debug for SubstringMatcher<'a> {
17    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
18        f.debug_struct("SubstringMatcher")
19            .field("pattern", &self.pattern)
20            .field("kind", &self.searcher.kind())
21            .field("start_kind", &self.searcher.start_kind())
22            .field("match_kind", &self.searcher.match_kind())
23            .finish()
24    }
25}
26impl<'a> SubstringMatcher<'a> {
27    /// Construct a new matcher from a given string
28    pub fn new(pattern: Span<Cow<'a, str>>, config: &Config) -> DiagResult<Self> {
29        Self::new_with_start_kind(pattern, StartKind::Unanchored, config)
30    }
31
32    pub fn new_with_start_kind(
33        pattern: Span<Cow<'a, str>>,
34        start_kind: StartKind,
35        config: &Config,
36    ) -> DiagResult<Self> {
37        assert!(
38            !pattern.is_empty(),
39            "an empty string is not a valid substring pattern"
40        );
41
42        let pattern =
43            pattern.map(|p| text::canonicalize_horizontal_whitespace(p, config.strict_whitespace));
44
45        let (span, pattern) = pattern.into_parts();
46        let mut builder = AhoCorasickBuilder::new();
47        let searcher = builder
48            .match_kind(MatchKind::LeftmostLongest)
49            .start_kind(start_kind)
50            .kind(Some(AhoCorasickKind::DFA))
51            .ascii_case_insensitive(config.ignore_case)
52            .build([pattern.as_ref()])
53            .map_err(|err| {
54                let diag = Diag::new("failed to build aho-corasick searcher")
55                    .with_help("this pattern was constructed as a DFA, with leftmost-longest match semantics, \
56                                it is possible a less restrictive configuration would succeed")
57                    .and_label(Label::new(span, err.to_string()));
58                Report::from(diag)
59            })?;
60        Ok(Self {
61            span,
62            pattern: Span::new(span, pattern),
63            searcher,
64        })
65    }
66}
67impl<'a> MatcherMut for SubstringMatcher<'a> {
68    fn try_match_mut<'input, 'context, C>(
69        &self,
70        input: Input<'input>,
71        context: &mut C,
72    ) -> DiagResult<MatchResult<'input>>
73    where
74        C: Context<'input, 'context> + ?Sized,
75    {
76        self.try_match(input, context)
77    }
78}
79impl<'a> Matcher for SubstringMatcher<'a> {
80    fn try_match<'input, 'context, C>(
81        &self,
82        input: Input<'input>,
83        context: &C,
84    ) -> DiagResult<MatchResult<'input>>
85    where
86        C: Context<'input, 'context> + ?Sized,
87    {
88        if let Some(matched) = self.searcher.find(input) {
89            Ok(MatchResult::ok(MatchInfo::new(matched.range(), self.span)))
90        } else {
91            Ok(MatchResult::failed(
92                CheckFailedError::MatchNoneButExpected {
93                    span: self.span,
94                    match_file: context.match_file(),
95                    note: None,
96                },
97            ))
98        }
99    }
100}
101impl<'a> Spanned for SubstringMatcher<'a> {
102    fn span(&self) -> SourceSpan {
103        self.span
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_substring_matcher() -> DiagResult<()> {
113        let mut context = TestContext::new();
114        context.with_checks("CHECK: Name: bar").with_input(
115            "
116Name: foo
117Field: 1
118
119Name: bar
120Field: 2
121"
122            .trim_start(),
123        );
124
125        let pattern = Span::new(8..10, Cow::Borrowed("Name: bar"));
126        let matcher =
127            SubstringMatcher::new(pattern, &context.config).expect("expected pattern to be valid");
128        let mctx = context.match_context();
129        let input = mctx.search();
130        let result = matcher.try_match(input, &mctx)?;
131        let info = result.info.expect("expected match");
132        assert_eq!(info.span.offset(), 20);
133        assert_eq!(info.span.len(), 9);
134        assert_eq!(
135            input.as_str(info.span.offset()..(info.span.offset() + info.span.len())),
136            "Name: bar"
137        );
138
139        Ok(())
140    }
141}