Repository: rust-analyzer/rust-analyzer
Pull request #5161 (2020-07-01 08:52:38)

SSR: Add initial support for placeholder constraints


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e1d2475e22..61ae8157a7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1248,6 +1248,7 @@ dependencies = [
  "ra_syntax",
  "ra_text_edit",
  "rustc-hash",
+ "test_utils",
 ]
 
 [[package]]
diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs
index 9f8e540c02..b3e9e5dfe1 100644
--- a/crates/ra_ide/src/ssr.rs
+++ b/crates/ra_ide/src/ssr.rs
@@ -10,6 +10,18 @@ use ra_ssr::{MatchFinder, SsrError, SsrRule};
 // 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.
 // 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`.
 //
 // ```rust
diff --git a/crates/ra_ssr/Cargo.toml b/crates/ra_ssr/Cargo.toml
index 3c2f15a833..fe098aaee3 100644
--- a/crates/ra_ssr/Cargo.toml
+++ b/crates/ra_ssr/Cargo.toml
@@ -17,3 +17,4 @@ ra_db = { path = "../ra_db" }
 ra_ide_db = { path = "../ra_ide_db" }
 hir = { path = "../ra_hir", package = "ra_hir" }
 rustc-hash = "1.1.0"
+test_utils = { path = "../test_utils" }
diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs
index 53d802e775..ce53d46d29 100644
--- a/crates/ra_ssr/src/matching.rs
+++ b/crates/ra_ssr/src/matching.rs
@@ -2,7 +2,7 @@
 //! process of matching, placeholder values are recorded.
 
 use crate::{
-    parsing::{Placeholder, SsrTemplate},
+    parsing::{Constraint, NodeKind, Placeholder, SsrTemplate},
     SsrMatches, SsrPattern, SsrRule,
 };
 use hir::Semantics;
@@ -11,6 +11,7 @@ use ra_syntax::ast::{AstNode, AstToken};
 use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken};
 use rustc_hash::FxHashMap;
 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
 // 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) =
             match_inputs.get_placeholder(&SyntaxElement::Node(pattern.clone()))
         {
+            for constraint in &placeholder.constraints {
+                self.check_constraint(constraint, code)?;
+            }
             if self.match_out.is_none() {
                 return Ok(());
             }
@@ -292,6 +296,24 @@ impl<'db, 'sema> MatchState<'db, 'sema> {
         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
     /// them.
     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.
 fn only_ident(element: SyntaxElement) -> Option<SyntaxToken> {
     match element {
diff --git a/crates/ra_ssr/src/parsing.rs b/crates/ra_ssr/src/parsing.rs
index 04d46bd326..5ea1256165 100644
--- a/crates/ra_ssr/src/parsing.rs
+++ b/crates/ra_ssr/src/parsing.rs
@@ -6,7 +6,7 @@
 //! e.g. expressions, type references etc.
 
 use crate::{SsrError, SsrPattern, SsrRule};
-use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind};
+use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, T};
 use rustc_hash::{FxHashMap, FxHashSet};
 use std::str::FromStr;
 
@@ -39,6 +39,18 @@ pub(crate) struct Placeholder {
     pub(crate) ident: SmolStr,
     /// 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>,
+}
+
+#[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)]
@@ -149,7 +161,7 @@ fn parse_pattern(pattern_str: &str) -> Result<Vec<PatternElement>, SsrError> {
     let mut placeholder_names = FxHashSet::default();
     let mut tokens = tokenize(pattern_str)?.into_iter();
     while let Some(token) = tokens.next() {
-        if token.kind == SyntaxKind::DOLLAR {
+        if token.kind == T![$] {
             let placeholder = parse_placeholder(&mut tokens)?;
             if !placeholder_names.insert(placeholder.ident.clone()) {
                 bail!("Name `{}` repeats more than once", placeholder.ident);
@@ -177,6 +189,9 @@ fn validate_rule(rule: &SsrRule) -> Result<(), SsrError> {
             if !defined_placeholders.contains(&placeholder.ident) {
                 undefined.push(format!("${}", placeholder.ident));
             }
+            if !placeholder.constraints.is_empty() {
+                bail!("Replacement placeholders cannot have constraints");
+            }
         }
     }
     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> {
     let mut name = None;
+    let mut constraints = Vec::new();
     if let Some(token) = tokens.next() {
         match token.kind {
             SyntaxKind::IDENT => {
                 name = Some(token.text);
             }
+            T!['{'] => {
+                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 {
+                        T![:] => {
+                            constraints.push(parse_constraint(tokens)?);
+                        }
+                        T!['}'] => 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"))?;
-    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 {
-    fn new(name: SmolStr) -> Self {
-        Self { stand_in_name: format!("__placeholder_{}", name), ident: name }
+    fn new(name: SmolStr, constraints: Vec<Constraint>) -> Self {
+        Self { stand_in_name: format!("__placeholder_{}", name), constraints, ident: name }
     }
 }
 
@@ -241,31 +323,31 @@ mod tests {
             PatternElement::Token(Token { kind, text: SmolStr::new(text) })
         }
         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();
         assert_eq!(
             result.pattern.raw.tokens,
             vec![
                 token(SyntaxKind::IDENT, "foo"),
-                token(SyntaxKind::L_PAREN, "("),
+                token(T!['('], "("),
                 placeholder("a"),
-                token(SyntaxKind::COMMA, ","),
+                token(T![,], ","),
                 token(SyntaxKind::WHITESPACE, " "),
                 placeholder("b"),
-                token(SyntaxKind::R_PAREN, ")"),
+                token(T![')'], ")"),
             ]
         );
         assert_eq!(
             result.template.tokens,
             vec![
                 token(SyntaxKind::IDENT, "bar"),
-                token(SyntaxKind::L_PAREN, "("),
+                token(T!['('], "("),
                 placeholder("b"),
-                token(SyntaxKind::COMMA, ","),
+                token(T![,], ","),
                 token(SyntaxKind::WHITESPACE, " "),
                 placeholder("a"),
-                token(SyntaxKind::R_PAREN, ")"),
+                token(T![')'], ")"),
             ]
         );
     }


Test patch:
diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs
index c692c97e2e..9568d44322 100644
--- a/crates/ra_ssr/src/tests.rs
+++ b/crates/ra_ssr/src/tests.rs
@@ -1,5 +1,6 @@
 use crate::{MatchFinder, SsrRule};
 use ra_db::{FileId, SourceDatabaseExt};
+use test_utils::mark;
 
 fn parse_error_text(query: &str) -> String {
     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)"]);
 }
 
+#[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]
 fn match_reordered_struct_instantiation() {
     assert_matches(