//! This file contains code for parsing SSR rules, which look something like `foo($a) ==>> bar($b)`.
//! We first split everything before and after the separator `==>>`. Next, both the search pattern
//! and the replacement template get tokenized by the Rust tokenizer. Tokens are then searched for
//! placeholders, which start with `$`. For replacement templates, this is the final form. For
//! search patterns, we go further and parse the pattern as each kind of thing that we can match.
//! e.g. expressions, type references etc.
use crate::errors::bail;
use crate::{SsrError, SsrPattern, SsrRule};
use rustc_hash::{FxHashMap, FxHashSet};
use std::{fmt::Display, str::FromStr};
use syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, T};
#[derive(Debug)]
pub(crate) struct ParsedRule {
pub(crate) placeholders_by_stand_in: FxHashMap<SmolStr, Placeholder>,
pub(crate) pattern: SyntaxNode,
pub(crate) template: Option<SyntaxNode>,
}
#[derive(Debug)]
pub(crate) struct RawPattern {
tokens: Vec<PatternElement>,
}
// Part of a search or replace pattern.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum PatternElement {
Token(Token),
Placeholder(Placeholder),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Placeholder {
/// The name of this placeholder. e.g. for "$a", this would be "a"
pub(crate) ident: Var,
/// A unique name used in place of this placeholder when we parse the pattern as Rust code.
stand_in_name: String,
pub(crate) constraints: Vec<Constraint>,
}
/// Represents a `$var` in an SSR query.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct Var(pub(crate) String);
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Constraint {
Kind(NodeKind),
Not(Box<Constraint>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum NodeKind {
Literal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Token {
kind: SyntaxKind,
pub(crate) text: SmolStr,
}
impl ParsedRule {
fn new(
pattern: &RawPattern,
template: Option<&RawPattern>,
) -> Result<Vec<ParsedRule>, SsrError> {
let raw_pattern = pattern.as_rust_code();
let raw_template = template.map(|t| t.as_rust_code());
let raw_template = raw_template.as_deref();
let mut builder = RuleBuilder {
placeholders_by_stand_in: pattern.placeholders_by_stand_in(),
rules: Vec::new(),
};
let raw_template_stmt = raw_template.map(ast::Stmt::parse);
if let raw_template_expr @ Some(Ok(_)) = raw_template.map(ast::Expr::parse) {
builder.try_add(ast::Expr::parse(&raw_pattern), raw_template_expr);
} else {
builder.try_add(ast::Expr::parse(&raw_pattern), raw_template_stmt.clone());
}
builder.try_add(ast::Type::parse(&raw_pattern), raw_template.map(ast::Type::parse));
builder.try_add(ast::Item::parse(&raw_pattern), raw_template.map(ast::Item::parse));
builder.try_add(ast::Path::parse(&raw_pattern), raw_template.map(ast::Path::parse));
builder.try_add(ast::Pat::parse(&raw_pattern), raw_template.map(ast::Pat::parse));
builder.try_add(ast::Stmt::parse(&raw_pattern), raw_template_stmt);
builder.build()
}
}
struct RuleBuilder {
placeholders_by_stand_in: FxHashMap<SmolStr, Placeholder>,
rules: Vec<ParsedRule>,
}
impl RuleBuilder {
fn try_add<T: AstNode, T2: AstNode>(
&mut self,
pattern: Result<T, ()>,
template: Option<Result<T2, ()>>,
) {
match (pattern, template) {
(Ok(pattern), Some(Ok(template))) => self.rules.push(ParsedRule {
placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
pattern: pattern.syntax().clone(),
template: Some(template.syntax().clone()),
}),
(Ok(pattern), None) => self.rules.push(ParsedRule {
placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
pattern: pattern.syntax().clone(),
template: None,
}),
_ => {}
}
}
fn build(mut self) -> Result<Vec<ParsedRule>, SsrError> {
if self.rules.is_empty() {
bail!("Not a valid Rust expression, type, item, path or pattern");
}
// If any rules contain paths, then we reject any rules that don't contain paths. Allowing a
// mix leads to strange semantics, since the path-based rules only match things where the
// path refers to semantically the same thing, whereas the non-path-based rules could match
// anything. Specifically, if we have a rule like `foo ==>> bar` we only want to match the
// `foo` that is in the current scope, not any `foo`. However "foo" can be parsed as a
// pattern (IDENT_PAT -> NAME -> IDENT). Allowing such a rule through would result in
// renaming everything called `foo` to `bar`. It'd also be slow, since without a path, we'd
// have to use the slow-scan search mechanism.
if self.rules.iter().any(|rule| contains_path(&rule.pattern)) {
let old_len = self.rules.len();
self.rules.retain(|rule| contains_path(&rule.pattern));
if self.rules.len() < old_len {
cov_mark::hit!(pattern_is_a_single_segment_path);
}
}
Ok(self.rules)
}
}
/// Returns whether there are any paths in `node`.
fn contains_path(node: &SyntaxNode) -> bool {
node.kind() == SyntaxKind::PATH
|| node.descendants().any(|node| node.kind() == SyntaxKind::PATH)
}
impl FromStr for SsrRule {
type Err = SsrError;
fn from_str(query: &str) -> Result<SsrRule, SsrError> {
let mut it = query.split("==>>");
let pattern = it.next().expect("at least empty string").trim();
let template = it