litcheck_filecheck/pattern/matcher/matchers/
substring.rs1use aho_corasick::{AhoCorasick, AhoCorasickBuilder, AhoCorasickKind, MatchKind, StartKind};
2
3use crate::common::*;
4
5pub struct SubstringMatcher<'a> {
8 span: SourceSpan,
11 pattern: Span<Cow<'a, str>>,
13 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 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}