Skip to main content

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 = pattern
43            .map(|p| text::canonicalize_horizontal_whitespace(p, config.options.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.options.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
67    pub fn pattern(&self) -> &str {
68        self.pattern.inner().as_ref()
69    }
70}
71impl<'a> MatcherMut for SubstringMatcher<'a> {
72    fn try_match_mut<'input, 'context, C>(
73        &self,
74        input: Input<'input>,
75        context: &mut C,
76    ) -> DiagResult<MatchResult<'input>>
77    where
78        C: Context<'input, 'context> + ?Sized,
79    {
80        self.try_match(input, context)
81    }
82}
83impl<'a> Matcher for SubstringMatcher<'a> {
84    fn try_match<'input, 'context, C>(
85        &self,
86        input: Input<'input>,
87        context: &C,
88    ) -> DiagResult<MatchResult<'input>>
89    where
90        C: Context<'input, 'context> + ?Sized,
91    {
92        if let Some(matched) = self.searcher.find(input) {
93            let span = SourceSpan::from_range_unchecked(input.source_id(), matched.range());
94            Ok(MatchResult::ok(MatchInfo::new(span, self.span)))
95        } else {
96            Ok(MatchResult::failed(
97                CheckFailedError::MatchNoneButExpected {
98                    span: self.span,
99                    match_file: context.source_file(self.span.source_id()).unwrap(),
100                    note: None,
101                },
102            ))
103        }
104    }
105}
106impl<'a> Spanned for SubstringMatcher<'a> {
107    fn span(&self) -> SourceSpan {
108        self.span
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use crate::source_file;
115
116    use super::*;
117
118    #[test]
119    fn test_substring_matcher() -> DiagResult<()> {
120        let mut context = TestContext::new();
121        let match_file = source_file!(context.config, "CHECK: Name: bar");
122        let input_file = source_file!(
123            context.config,
124            "
125Name: foo
126Field: 1
127
128Name: bar
129Field: 2
130"
131            .trim_start()
132        );
133        context
134            .with_checks(match_file)
135            .with_input(input_file.clone());
136
137        let pattern = Span::new(
138            SourceSpan::from_range_unchecked(input_file.id(), 8..10),
139            Cow::Borrowed("Name: bar"),
140        );
141        let matcher =
142            SubstringMatcher::new(pattern, &context.config).expect("expected pattern to be valid");
143        let mctx = context.match_context();
144        let input = mctx.search();
145        let result = matcher.try_match(input, &mctx)?;
146        let info = result.info.expect("expected match");
147        assert_eq!(info.span.start().to_u32(), 20);
148        assert_eq!(info.span.len(), 9);
149        assert_eq!(input.as_str(info.span.into_slice_index()), "Name: bar");
150
151        Ok(())
152    }
153}