SSR: Add initial support for placeholder constraints

This commit is contained in:
David Lattimore 2020-06-23 19:07:42 +10:00
parent d34fd372bb
commit 3d9997889b
6 changed files with 156 additions and 6 deletions

1
Cargo.lock generated
View file

@ -1248,6 +1248,7 @@ dependencies = [
"ra_syntax", "ra_syntax",
"ra_text_edit", "ra_text_edit",
"rustc-hash", "rustc-hash",
"test_utils",
] ]
[[package]] [[package]]

View file

@ -10,6 +10,18 @@ use ra_ssr::{MatchFinder, SsrError, SsrRule};
// The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`. // The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`.
// A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement. // A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement.
// Within a macro call, a placeholder will match up until whatever token follows the placeholder. // Within a macro call, a placeholder will match up until whatever token follows the placeholder.
//
// Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`.
//
// Supported constraints:
//
// |===
// | Constraint | Restricts placeholder
//
// | kind(literal) | Is a literal (e.g. `42` or `"forty two"`)
// | not(a) | Negates the constraint `a`
// |===
//
// Available via the command `rust-analyzer.ssr`. // Available via the command `rust-analyzer.ssr`.
// //
// ```rust // ```rust

View file

@ -17,3 +17,4 @@ ra_db = { path = "../ra_db" }
ra_ide_db = { path = "../ra_ide_db" } ra_ide_db = { path = "../ra_ide_db" }
hir = { path = "../ra_hir", package = "ra_hir" } hir = { path = "../ra_hir", package = "ra_hir" }
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
test_utils = { path = "../test_utils" }

View file

@ -2,7 +2,7 @@
//! process of matching, placeholder values are recorded. //! process of matching, placeholder values are recorded.
use crate::{ use crate::{
parsing::{Placeholder, SsrTemplate}, parsing::{Constraint, NodeKind, Placeholder, SsrTemplate},
SsrMatches, SsrPattern, SsrRule, SsrMatches, SsrPattern, SsrRule,
}; };
use hir::Semantics; use hir::Semantics;
@ -11,6 +11,7 @@ use ra_syntax::ast::{AstNode, AstToken};
use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken}; use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::{cell::Cell, iter::Peekable}; use std::{cell::Cell, iter::Peekable};
use test_utils::mark;
// Creates a match error. If we're currently attempting to match some code that we thought we were // Creates a match error. If we're currently attempting to match some code that we thought we were
// going to match, as indicated by the --debug-snippet flag, then populate the reason field. // going to match, as indicated by the --debug-snippet flag, then populate the reason field.
@ -169,6 +170,9 @@ impl<'db, 'sema> MatchState<'db, 'sema> {
if let Some(placeholder) = if let Some(placeholder) =
match_inputs.get_placeholder(&SyntaxElement::Node(pattern.clone())) match_inputs.get_placeholder(&SyntaxElement::Node(pattern.clone()))
{ {
for constraint in &placeholder.constraints {
self.check_constraint(constraint, code)?;
}
if self.match_out.is_none() { if self.match_out.is_none() {
return Ok(()); return Ok(());
} }
@ -292,6 +296,24 @@ impl<'db, 'sema> MatchState<'db, 'sema> {
Ok(()) Ok(())
} }
fn check_constraint(
&self,
constraint: &Constraint,
code: &SyntaxNode,
) -> Result<(), MatchFailed> {
match constraint {
Constraint::Kind(kind) => {
kind.matches(code)?;
}
Constraint::Not(sub) => {
if self.check_constraint(&*sub, code).is_ok() {
fail_match!("Constraint {:?} failed for '{}'", constraint, code.text());
}
}
}
Ok(())
}
/// We want to allow the records to match in any order, so we have special matching logic for /// We want to allow the records to match in any order, so we have special matching logic for
/// them. /// them.
fn attempt_match_record_field_list( fn attempt_match_record_field_list(
@ -515,6 +537,21 @@ impl SsrPattern {
} }
} }
impl NodeKind {
fn matches(&self, node: &SyntaxNode) -> Result<(), MatchFailed> {
let ok = match self {
Self::Literal => {
mark::hit!(literal_constraint);
ast::Literal::can_cast(node.kind())
}
};
if !ok {
fail_match!("Code '{}' isn't of kind {:?}", node.text(), self);
}
Ok(())
}
}
// If `node` contains nothing but an ident then return it, otherwise return None. // If `node` contains nothing but an ident then return it, otherwise return None.
fn only_ident(element: SyntaxElement) -> Option<SyntaxToken> { fn only_ident(element: SyntaxElement) -> Option<SyntaxToken> {
match element { match element {

View file

@ -39,6 +39,18 @@ pub(crate) struct Placeholder {
pub(crate) ident: SmolStr, pub(crate) ident: SmolStr,
/// A unique name used in place of this placeholder when we parse the pattern as Rust code. /// A unique name used in place of this placeholder when we parse the pattern as Rust code.
stand_in_name: String, stand_in_name: String,
pub(crate) constraints: Vec<Constraint>,
}
#[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)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -177,6 +189,9 @@ fn validate_rule(rule: &SsrRule) -> Result<(), SsrError> {
if !defined_placeholders.contains(&placeholder.ident) { if !defined_placeholders.contains(&placeholder.ident) {
undefined.push(format!("${}", placeholder.ident)); undefined.push(format!("${}", placeholder.ident));
} }
if !placeholder.constraints.is_empty() {
bail!("Replacement placeholders cannot have constraints");
}
} }
} }
if !undefined.is_empty() { if !undefined.is_empty() {
@ -205,23 +220,90 @@ fn tokenize(source: &str) -> Result<Vec<Token>, SsrError> {
fn parse_placeholder(tokens: &mut std::vec::IntoIter<Token>) -> Result<Placeholder, SsrError> { fn parse_placeholder(tokens: &mut std::vec::IntoIter<Token>) -> Result<Placeholder, SsrError> {
let mut name = None; let mut name = None;
let mut constraints = Vec::new();
if let Some(token) = tokens.next() { if let Some(token) = tokens.next() {
match token.kind { match token.kind {
SyntaxKind::IDENT => { SyntaxKind::IDENT => {
name = Some(token.text); name = Some(token.text);
} }
SyntaxKind::L_CURLY => {
let token =
tokens.next().ok_or_else(|| SsrError::new("Unexpected end of placeholder"))?;
if token.kind == SyntaxKind::IDENT {
name = Some(token.text);
}
loop {
let token = tokens
.next()
.ok_or_else(|| SsrError::new("Placeholder is missing closing brace '}'"))?;
match token.kind {
SyntaxKind::COLON => {
constraints.push(parse_constraint(tokens)?);
}
SyntaxKind::R_CURLY => break,
_ => bail!("Unexpected token while parsing placeholder: '{}'", token.text),
}
}
}
_ => { _ => {
bail!("Placeholders should be $name"); bail!("Placeholders should either be $name or ${name:constraints}");
} }
} }
} }
let name = name.ok_or_else(|| SsrError::new("Placeholder ($) with no name"))?; let name = name.ok_or_else(|| SsrError::new("Placeholder ($) with no name"))?;
Ok(Placeholder::new(name)) Ok(Placeholder::new(name, constraints))
}
fn parse_constraint(tokens: &mut std::vec::IntoIter<Token>) -> Result<Constraint, SsrError> {
let constraint_type = tokens
.next()
.ok_or_else(|| SsrError::new("Found end of placeholder while looking for a constraint"))?
.text
.to_string();
match constraint_type.as_str() {
"kind" => {
expect_token(tokens, "(")?;
let t = tokens.next().ok_or_else(|| {
SsrError::new("Unexpected end of constraint while looking for kind")
})?;
if t.kind != SyntaxKind::IDENT {
bail!("Expected ident, found {:?} while parsing kind constraint", t.kind);
}
expect_token(tokens, ")")?;
Ok(Constraint::Kind(NodeKind::from(&t.text)?))
}
"not" => {
expect_token(tokens, "(")?;
let sub = parse_constraint(tokens)?;
expect_token(tokens, ")")?;
Ok(Constraint::Not(Box::new(sub)))
}
x => bail!("Unsupported constraint type '{}'", x),
}
}
fn expect_token(tokens: &mut std::vec::IntoIter<Token>, expected: &str) -> Result<(), SsrError> {
if let Some(t) = tokens.next() {
if t.text == expected {
return Ok(());
}
bail!("Expected {} found {}", expected, t.text);
}
bail!("Expected {} found end of stream");
}
impl NodeKind {
fn from(name: &SmolStr) -> Result<NodeKind, SsrError> {
Ok(match name.as_str() {
"literal" => NodeKind::Literal,
_ => bail!("Unknown node kind '{}'", name),
})
}
} }
impl Placeholder { impl Placeholder {
fn new(name: SmolStr) -> Self { fn new(name: SmolStr, constraints: Vec<Constraint>) -> Self {
Self { stand_in_name: format!("__placeholder_{}", name), ident: name } Self { stand_in_name: format!("__placeholder_{}", name), constraints, ident: name }
} }
} }
@ -241,7 +323,7 @@ mod tests {
PatternElement::Token(Token { kind, text: SmolStr::new(text) }) PatternElement::Token(Token { kind, text: SmolStr::new(text) })
} }
fn placeholder(name: &str) -> PatternElement { fn placeholder(name: &str) -> PatternElement {
PatternElement::Placeholder(Placeholder::new(SmolStr::new(name))) PatternElement::Placeholder(Placeholder::new(SmolStr::new(name), Vec::new()))
} }
let result: SsrRule = "foo($a, $b) ==>> bar($b, $a)".parse().unwrap(); let result: SsrRule = "foo($a, $b) ==>> bar($b, $a)".parse().unwrap();
assert_eq!( assert_eq!(

View file

@ -1,5 +1,6 @@
use crate::{MatchFinder, SsrRule}; use crate::{MatchFinder, SsrRule};
use ra_db::{FileId, SourceDatabaseExt}; use ra_db::{FileId, SourceDatabaseExt};
use test_utils::mark;
fn parse_error_text(query: &str) -> String { fn parse_error_text(query: &str) -> String {
format!("{}", query.parse::<SsrRule>().unwrap_err()) format!("{}", query.parse::<SsrRule>().unwrap_err())
@ -301,6 +302,22 @@ fn match_pattern() {
assert_matches("Some($a)", "fn f() {if let Some(x) = foo() {}}", &["Some(x)"]); assert_matches("Some($a)", "fn f() {if let Some(x) = foo() {}}", &["Some(x)"]);
} }
#[test]
fn literal_constraint() {
mark::check!(literal_constraint);
let code = r#"
fn f1() {
let x1 = Some(42);
let x2 = Some("foo");
let x3 = Some(x1);
let x4 = Some(40 + 2);
let x5 = Some(true);
}
"#;
assert_matches("Some(${a:kind(literal)})", code, &["Some(42)", "Some(\"foo\")", "Some(true)"]);
assert_matches("Some(${a:not(kind(literal))})", code, &["Some(x1)", "Some(40 + 2)"]);
}
#[test] #[test]
fn match_reordered_struct_instantiation() { fn match_reordered_struct_instantiation() {
assert_matches( assert_matches(