use crate::match_tree::does_node_match_exactly;
use crate::matcher::{KindMatcher, Pattern, RegexMatcher};
use crate::{Doc, Node};
use std::borrow::Cow;
use std::collections::HashMap;
pub type MetaVariableID = String;
#[derive(Clone)]
pub struct MetaVarEnv<'tree, D: Doc> {
single_matched: HashMap<MetaVariableID, Node<'tree, D>>,
multi_matched: HashMap<MetaVariableID, Vec<Node<'tree, D>>>,
}
impl<'tree, D: Doc> MetaVarEnv<'tree, D> {
pub fn new() -> Self {
Self {
single_matched: HashMap::new(),
multi_matched: HashMap::new(),
}
}
pub fn insert(&mut self, id: MetaVariableID, ret: Node<'tree, D>) -> Option<&mut Self> {
if !self.match_variable(&id, ret.clone()) {
return None;
}
self.single_matched.insert(id, ret);
Some(self)
}
pub fn insert_multi(
&mut self,
id: MetaVariableID,
ret: Vec<Node<'tree, D>>,
) -> Option<&mut Self> {
self.multi_matched.insert(id, ret);
Some(self)
}
pub fn get(&self, var: &MetaVariable) -> Option<MatchResult<'_, 'tree, D>> {
match var {
MetaVariable::Named(n, _) => self.single_matched.get(n).map(MatchResult::Single),
MetaVariable::NamedEllipsis(n) => self.multi_matched.get(n).map(MatchResult::Multi),
_ => None,
}
}
pub fn get_match(&self, var: &str) -> Option<&'_ Node<'tree, D>> {
self.single_matched.get(var)
}
pub fn get_multiple_matches(&self, var: &str) -> Vec<Node<'tree, D>> {
self.multi_matched.get(var).cloned().unwrap_or_default()
}
pub fn add_label(&mut self, label: &str, node: Node<'tree, D>) {
self
.multi_matched
.entry(label.into())
.or_insert_with(Vec::new)
.push(node);
}
pub fn get_labels(&self, label: &str) -> Option<&Vec<Node<'tree, D>>> {
self.multi_matched.get(label)
}
pub fn get_matched_variables(&self) -> impl Iterator<Item = MetaVariable> + '_ {
let single = self
.single_matched
.keys()
.cloned()
.map(|n| MetaVariable::Named(n, false));
let multi = self
.multi_matched
.keys()
.cloned()
.map(MetaVariable::NamedEllipsis);
single.chain(multi)
}
pub fn match_constraints(
&self,
var_matchers: &MetaVarMatchers<impl Doc<Lang = D::Lang>>,
) -> bool {
for (var_id, candidate) in &self.single_matched {
if let Some(m) = var_matchers.0.get(var_id) {
if !m.matches(candidate.clone()) {
return false;
}
}
}
true
}
fn match_variable(&self, id: &MetaVariableID, candidate: Node<D>) -> bool {
if let Some(m) = self.single_matched.get(id) {
return does_node_match_exactly(m, candidate);
}
true
}
}
impl<'tree, D: Doc> Default for MetaVarEnv<'tree, D> {
fn default() -> Self {
Self::new()
}
}
impl<'tree, D: Doc> From<MetaVarEnv<'tree, D>> for HashMap<String, String> {
fn from(env: MetaVarEnv<'tree, D>) -> Self {
let mut ret = HashMap::new();
for (id, node) in env.single_matched {
ret.insert(id, node.text().into());
}
for (id, nodes) in env.multi_matched {
let s: Vec<_> = nodes.iter().map(|n| n.text()).collect();
let s = s.join(", ");
ret.insert(id, format!("[{s}]"));
}
ret
}
}
pub enum MatchResult<'a, 'tree, D: Doc> {
Single(&'a Node<'tree, D>),
Multi(&'a Vec<Node<'tree, D>>),
}
#[derive(Debug, PartialEq, Eq)]
pub enum MetaVariable {
Named(MetaVariableID, bool),
Anonymous(bool),
Ellipsis,
NamedEllipsis(MetaVariableID),
}
#[derive(Clone)]
pub struct MetaVarMatchers<D: Doc>(HashMap<MetaVariableID, MetaVarMatcher<D>>);
impl<D: Doc> MetaVarMatchers<D> {
pub fn new() -> Self {
Self(HashMap::new())
}
pub fn insert(&mut self, var_id: MetaVariableID, matcher: MetaVarMatcher<D>) {
self.0.insert(var_id, matcher);
}
}
impl<D: Doc> Default for MetaVarMatchers<D> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub enum MetaVarMatcher<D: Doc> {
#[cfg(feature = "regex")]
Regex(RegexMatcher<D::Lang>),
Pattern(Pattern<D>),
Kind(KindMatcher<D::Lang>),
}
impl<D: Doc> MetaVarMatcher<D> {
pub fn matches(&self, candidate: Node<impl Doc<Lang = D::Lang>>) -> bool {
use crate::matcher::Matcher;
use MetaVarMatcher::*;
let mut env = Cow::Owned(MetaVarEnv::new());
match self {
#[cfg(feature = "regex")]
Regex(r) => r.match_node_with_env(candidate, &mut env).is_some(),
Pattern(p) => p.match_node_with_env(candidate, &mut env).is_some(),
Kind(k) => k.match_node_with_env(candidate, &mut env).is_some(),
}
}
}
pub(crate) fn extract_meta_var(src: &str, meta_char: char) -> Option<MetaVariable> {
use MetaVariable::*;
let ellipsis: String = std::iter::repeat(meta_char).take(3).collect();
if src == ellipsis {
return Some(Ellipsis);
}
if let Some(trimmed) = src.strip_prefix(&ellipsis) {
if !trimmed.chars().all(is_valid_meta_var_char) {
return None;
}
if trimmed.starts_with('_') {
return Some(Ellipsis);
} else {
return Some(NamedEllipsis(trimmed.to_owned()));
}
}
if !src.starts_with(meta_char) {
return None;
}
let trimmed = &src[meta_char.len_utf8()..];
let (trimmed, named) = if let Some(t) = trimmed.strip_prefix(meta_char) {
(t, false)
} else {
(trimmed, true)
};
if !trimmed.chars().all(is_valid_meta_var_char) {
return None;
}
if trimmed.starts_with('_') {
Some(Anonymous(named))
} else {
Some(Named(trimmed.to_owned(), named))
}
}
pub fn split_first_meta_var(src: &str, meta_char: char) -> (&str, &str) {
assert!(src.starts_with(meta_char));
let src = &src[meta_char.len_utf8()..];
if let Some(i) = src.find(|c| !is_valid_meta_var_char(c)) {
(&src[..i], &src[i..])
} else {
(src, "")
}
}
fn is_valid_meta_var_char(c: char) -> bool {
matches!(c, 'A'..='Z' | '_')
}
#[cfg(test)]
mod test {
use super::*;
use crate::language::{Language, Tsx};
use crate::{Pattern, StrDoc};
fn extract_var(s: &str) -> Option<MetaVariable> {
extract_meta_var(s, '$')
}
#[test]
fn test_match_var() {
use MetaVariable::*;
assert_eq!(extract_var("$$$"), Some(Ellipsis));
assert_eq!(extract_var("$ABC"), Some(Named("ABC".into(), true)));
assert_eq!(extract_var("$$ABC"), Some(Named("ABC".into(), false)));
assert_eq!(extract_var("$$$ABC"), Some(NamedEllipsis("ABC".into())));
assert_eq!(extract_var("$_"), Some(Anonymous(true)));
assert_eq!(extract_var("abc"), None);
assert_eq!(extract_var("$abc"), None);
}
fn match_constraints(pattern: &str, node: &str) -> bool {
let mut matchers: MetaVarMatchers<StrDoc<_>> = MetaVarMatchers(HashMap::new());
matchers.insert(
"A".to_string(),
MetaVarMatcher::Pattern(Pattern::new(pattern, Tsx)),
);
let mut env = MetaVarEnv::new();
let root = Tsx.ast_grep(node);
let node = root.root().child(0).unwrap().child(0).unwrap();
env.insert("A".to_string(), node);
env.match_constraints(&matchers)
}
#[test]
fn test_non_ascii_meta_var() {
let extract = |s| extract_meta_var(s, 'µ');
use MetaVariable::*;
assert_eq!(extract("µµµ"), Some(Ellipsis));
assert_eq!(extract("µABC"), Some(Named("ABC".into(), true)));
assert_eq!(extract("µµABC"), Some(Named("ABC".into(), false)));
assert_eq!(extract("µµµABC"), Some(NamedEllipsis("ABC".into())));
assert_eq!(extract("µ_"), Some(Anonymous(true)));
assert_eq!(extract("abc"), None);
assert_eq!(extract("µabc"), None);
}
#[test]
fn test_match_constraints() {
assert!(match_constraints("a + b", "a + b"));
}
#[test]
fn test_match_not_constraints() {
assert!(!match_constraints("a - b", "a + b"));
}
}