ast_grep_core/match_tree/
strictness.rs

1use crate::matcher::{kind_utils, PatternNode};
2use crate::meta_var::MetaVariable;
3use crate::node::Node;
4use crate::Doc;
5use std::iter::Peekable;
6use std::str::FromStr;
7
8#[derive(Clone)]
9pub enum MatchStrictness {
10  Cst,       // all nodes are matched
11  Smart,     // all nodes except source trivial nodes are matched.
12  Ast,       // only ast nodes are matched
13  Relaxed,   // ast-nodes excluding comments are matched
14  Signature, // ast-nodes excluding comments, without text
15  Template,  // similar to smart, but node kinds are ignored, only text is matched.
16}
17
18pub(crate) enum MatchOneNode {
19  MatchedBoth,
20  SkipBoth,
21  SkipGoal,
22  SkipCandidate,
23  NoMatch,
24}
25
26fn skip_comment(n: &Node<impl Doc>) -> bool {
27  n.kind().contains("comment")
28}
29
30fn skip_comment_or_unnamed(n: &Node<impl Doc>) -> bool {
31  if !n.is_named() {
32    return true;
33  }
34  skip_comment(n)
35}
36
37impl MatchStrictness {
38  pub(crate) fn should_skip_kind(&self) -> bool {
39    use MatchStrictness as M;
40    match self {
41      M::Template => true,
42      M::Cst => false,
43      M::Smart => false,
44      M::Ast => false,
45      M::Relaxed => false,
46      M::Signature => false,
47    }
48  }
49
50  fn should_skip_comment(&self) -> bool {
51    use MatchStrictness as M;
52    match self {
53      M::Cst | M::Smart | M::Ast => false,
54      M::Relaxed | M::Signature | M::Template => true,
55    }
56  }
57
58  pub(crate) fn match_terminal(
59    &self,
60    is_named: bool,
61    text: &str,
62    goal_kind: u16,
63    candidate: &Node<impl Doc>,
64  ) -> MatchOneNode {
65    use MatchStrictness as M;
66    let cand_kind = candidate.kind_id();
67    let is_kind_matched = kind_utils::are_kinds_matching(goal_kind, cand_kind);
68    // work around ast-grep/ast-grep#1419 and tree-sitter/tree-sitter-typescript#306
69    // tree-sitter-typescript has wrong span of unnamed node so text would not match
70    // just compare kind for unnamed node
71    if is_kind_matched && (!is_named || text == candidate.text()) {
72      return MatchOneNode::MatchedBoth;
73    }
74    if self.should_skip_comment() && skip_comment(candidate) {
75      return MatchOneNode::SkipCandidate;
76    }
77    let (skip_goal, skip_candidate) = match self {
78      M::Cst => (false, false),
79      M::Smart => (false, !candidate.is_named()),
80      M::Ast => (!is_named, !candidate.is_named()),
81      M::Relaxed => (!is_named, !candidate.is_named()),
82      M::Signature => {
83        if is_kind_matched {
84          return MatchOneNode::MatchedBoth;
85        }
86        (!is_named, !candidate.is_named())
87      }
88      M::Template => {
89        if text == candidate.text() {
90          return MatchOneNode::MatchedBoth;
91        } else {
92          (false, !candidate.is_named())
93        }
94      }
95    };
96    match (skip_goal, skip_candidate) {
97      (true, true) => MatchOneNode::SkipBoth,
98      (true, false) => MatchOneNode::SkipGoal,
99      (false, true) => MatchOneNode::SkipCandidate,
100      (false, false) => MatchOneNode::NoMatch,
101    }
102  }
103
104  pub(crate) fn should_skip_cand_for_metavar<D: Doc>(&self, candidate: &Node<D>) -> bool {
105    use MatchStrictness as M;
106    match self {
107      M::Cst | M::Ast | M::Smart => false,
108      M::Relaxed | M::Signature | M::Template => skip_comment(candidate),
109    }
110  }
111
112  // TODO: this is a method for working around trailing nodes after pattern is matched
113  pub(crate) fn should_skip_trailing<D: Doc>(&self, candidate: &Node<D>) -> bool {
114    use MatchStrictness as M;
115    match self {
116      M::Cst => false,
117      M::Smart => true,
118      M::Ast => false,
119      M::Relaxed => skip_comment_or_unnamed(candidate),
120      M::Signature => skip_comment_or_unnamed(candidate),
121      M::Template => skip_comment(candidate),
122    }
123  }
124
125  pub(crate) fn should_skip_goal<'p>(
126    &self,
127    goal_children: &mut Peekable<impl Iterator<Item = &'p PatternNode>>,
128  ) -> bool {
129    use MatchStrictness as M;
130    while let Some(pattern) = goal_children.peek() {
131      let skipped = match self {
132        M::Cst => false,
133        M::Smart | M::Template => match pattern {
134          PatternNode::MetaVar { meta_var } => match meta_var {
135            MetaVariable::Multiple => true,
136            MetaVariable::MultiCapture(_) => true,
137            MetaVariable::Dropped(_) => false,
138            MetaVariable::Capture(..) => false,
139          },
140          PatternNode::Terminal { .. } => false,
141          PatternNode::Internal { .. } => false,
142        },
143        M::Ast | M::Relaxed | M::Signature => match pattern {
144          PatternNode::MetaVar { meta_var } => match meta_var {
145            MetaVariable::Multiple => true,
146            MetaVariable::MultiCapture(_) => true,
147            MetaVariable::Dropped(named) => !named,
148            MetaVariable::Capture(_, named) => !named,
149          },
150          PatternNode::Terminal { is_named, .. } => !is_named,
151          PatternNode::Internal { .. } => false,
152        },
153      };
154      if !skipped {
155        return false;
156      }
157      goal_children.next();
158    }
159    true
160  }
161}
162
163impl FromStr for MatchStrictness {
164  type Err = &'static str;
165  fn from_str(s: &str) -> Result<Self, Self::Err> {
166    match s {
167      "cst" => Ok(MatchStrictness::Cst),
168      "smart" => Ok(MatchStrictness::Smart),
169      "ast" => Ok(MatchStrictness::Ast),
170      "relaxed" => Ok(MatchStrictness::Relaxed),
171      "signature" => Ok(MatchStrictness::Signature),
172      "template" => Ok(MatchStrictness::Template),
173      _ => Err("invalid strictness, valid options are: cst, smart, ast, relaxed, signature"),
174    }
175  }
176}
177
178#[cfg(test)]
179mod test {
180  use super::*;
181  use crate::language::Tsx;
182  use crate::{Pattern, Root};
183
184  fn test_match(p: &str, n: &str, strictness: MatchStrictness) -> bool {
185    let mut pattern = Pattern::new(p, Tsx);
186    pattern.strictness = strictness;
187    let root = Root::str(n, Tsx);
188    let node = root.root();
189    node.find(pattern).is_some()
190  }
191
192  fn template_pattern(p: &str, n: &str) -> bool {
193    test_match(p, n, MatchStrictness::Template)
194  }
195
196  #[test]
197  fn test_template_pattern() {
198    assert!(template_pattern("$A = $B", "a = b"));
199    assert!(template_pattern("$A = $B", "var a = b"));
200    assert!(template_pattern("$A = $B", "let a = b"));
201    assert!(template_pattern("$A = $B", "const a = b"));
202    assert!(template_pattern("$A = $B", "class A { a = b }"));
203  }
204
205  fn relaxed_pattern(p: &str, n: &str) -> bool {
206    test_match(p, n, MatchStrictness::Relaxed)
207  }
208
209  #[test]
210  fn test_ignore_comment() {
211    assert!(relaxed_pattern("$A($B)", "foo(bar /* .. */)"));
212    assert!(relaxed_pattern(
213      "$A($B)",
214      "
215      foo(
216        bar, // ..
217      )"
218    ));
219    assert!(relaxed_pattern("$A($B)", "foo(/* .. */ bar)"));
220    assert!(relaxed_pattern(
221      "$A($B)",
222      "
223      foo( // ..
224        bar
225      )"
226    ));
227  }
228}