From 3d9997889bfe536a96e70535ab208a6e7ff3bc12 Mon Sep 17 00:00:00 2001 From: David Lattimore Date: Tue, 23 Jun 2020 19:07:42 +1000 Subject: SSR: Add initial support for placeholder constraints --- crates/ra_ssr/src/matching.rs | 39 +++++++++++++++++- crates/ra_ssr/src/parsing.rs | 92 ++++++++++++++++++++++++++++++++++++++++--- crates/ra_ssr/src/tests.rs | 17 ++++++++ 3 files changed, 142 insertions(+), 6 deletions(-) (limited to 'crates/ra_ssr/src') diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs index 53d802e77..ce53d46d2 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 { match element { diff --git a/crates/ra_ssr/src/parsing.rs b/crates/ra_ssr/src/parsing.rs index 04d46bd32..0f4f88b7c 100644 --- a/crates/ra_ssr/src/parsing.rs +++ b/crates/ra_ssr/src/parsing.rs @@ -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, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Constraint { + Kind(NodeKind), + Not(Box), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum NodeKind { + Literal, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -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, SsrError> { fn parse_placeholder(tokens: &mut std::vec::IntoIter) -> Result { let mut name = None; + let mut constraints = Vec::new(); if let Some(token) = tokens.next() { match token.kind { SyntaxKind::IDENT => { 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"))?; - Ok(Placeholder::new(name)) + Ok(Placeholder::new(name, constraints)) +} + +fn parse_constraint(tokens: &mut std::vec::IntoIter) -> Result { + 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, 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 { + 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) -> Self { + Self { stand_in_name: format!("__placeholder_{}", name), constraints, ident: name } } } @@ -241,7 +323,7 @@ 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!( diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs index c692c97e2..9568d4432 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::().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( -- cgit v1.2.3