Skip to main content

ast_grep_core/match_tree/
strictness.rs

1use crate::Doc;
2use crate::matcher::{PatternNode, kind_utils};
3use crate::meta_var::MetaVariable;
4use crate::node::Node;
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 relaxed, 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.is_extra()
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::Ast => false,
54      M::Smart | 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    self.should_skip_comment() && skip_comment(candidate)
106  }
107
108  // TODO: this is a method for working around trailing nodes after pattern is matched
109  pub(crate) fn should_skip_trailing<D: Doc>(&self, candidate: &Node<D>) -> bool {
110    use MatchStrictness as M;
111    match self {
112      M::Cst => false,
113      M::Smart => true,
114      M::Ast => !candidate.is_named(),
115      M::Relaxed => skip_comment_or_unnamed(candidate),
116      M::Signature => skip_comment_or_unnamed(candidate),
117      M::Template => skip_comment(candidate),
118    }
119  }
120
121  pub(crate) fn should_skip_goal<'p>(
122    &self,
123    goal_children: &mut Peekable<impl Iterator<Item = &'p PatternNode>>,
124  ) -> bool {
125    use MatchStrictness as M;
126    while let Some(pattern) = goal_children.peek() {
127      let skipped = match self {
128        M::Cst => false,
129        M::Smart | M::Template => match pattern {
130          PatternNode::MetaVar { meta_var } => match meta_var {
131            MetaVariable::Multiple => true,
132            MetaVariable::MultiCapture(_) => true,
133            MetaVariable::Dropped(_) => false,
134            MetaVariable::Capture(..) => false,
135          },
136          PatternNode::Terminal { .. } => false,
137          PatternNode::Internal { .. } => false,
138        },
139        M::Ast | M::Relaxed | M::Signature => match pattern {
140          PatternNode::MetaVar { meta_var } => match meta_var {
141            MetaVariable::Multiple => true,
142            MetaVariable::MultiCapture(_) => true,
143            MetaVariable::Dropped(named) => !named,
144            MetaVariable::Capture(_, named) => !named,
145          },
146          PatternNode::Terminal { is_named, .. } => !is_named,
147          PatternNode::Internal { .. } => false,
148        },
149      };
150      if !skipped {
151        return false;
152      }
153      goal_children.next();
154    }
155    true
156  }
157}
158
159impl FromStr for MatchStrictness {
160  type Err = &'static str;
161  fn from_str(s: &str) -> Result<Self, Self::Err> {
162    match s {
163      "cst" => Ok(MatchStrictness::Cst),
164      "smart" => Ok(MatchStrictness::Smart),
165      "ast" => Ok(MatchStrictness::Ast),
166      "relaxed" => Ok(MatchStrictness::Relaxed),
167      "signature" => Ok(MatchStrictness::Signature),
168      "template" => Ok(MatchStrictness::Template),
169      _ => Err("invalid strictness, valid options are: cst, smart, ast, relaxed, signature"),
170    }
171  }
172}
173
174#[cfg(test)]
175mod test {
176  use super::*;
177  use crate::language::Tsx;
178  use crate::{Pattern, Root};
179
180  fn test_match(p: &str, n: &str, strictness: MatchStrictness) -> bool {
181    let mut pattern = Pattern::new(p, Tsx);
182    pattern.strictness = strictness;
183    let root = Root::str(n, Tsx);
184    let node = root.root();
185    node.find(pattern).is_some()
186  }
187
188  fn template_pattern(p: &str, n: &str) -> bool {
189    test_match(p, n, MatchStrictness::Template)
190  }
191
192  #[test]
193  fn test_template_pattern() {
194    assert!(template_pattern("$A = $B", "a = b"));
195    assert!(template_pattern("$A = $B", "var a = b"));
196    assert!(template_pattern("$A = $B", "let a = b"));
197    assert!(template_pattern("$A = $B", "const a = b"));
198    assert!(template_pattern("$A = $B", "class A { a = b }"));
199  }
200
201  fn relaxed_pattern(p: &str, n: &str) -> bool {
202    test_match(p, n, MatchStrictness::Relaxed)
203  }
204
205  #[test]
206  fn test_ignore_comment() {
207    assert!(relaxed_pattern("$A($B)", "foo(bar /* .. */)"));
208    assert!(relaxed_pattern(
209      "$A($B)",
210      "
211      foo(
212        bar, // ..
213      )"
214    ));
215    assert!(relaxed_pattern("$A($B)", "foo(/* .. */ bar)"));
216    assert!(relaxed_pattern(
217      "$A($B)",
218      "
219      foo( // ..
220        bar
221      )"
222    ));
223  }
224
225  #[test]
226  fn test_ast_trailing_comma() {
227    assert!(test_match("foo(bar)", "foo(bar,)", MatchStrictness::Ast));
228  }
229}