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