From 95f8310514238fc2c3b4d1ddecbd1b79d2c963a2 Mon Sep 17 00:00:00 2001 From: David Lattimore Date: Tue, 30 Jun 2020 15:55:20 +1000 Subject: Structured search debugging --- crates/ra_ssr/src/lib.rs | 162 +++++++++++++++++++++++++++++- crates/ra_ssr/src/matching.rs | 10 +- crates/ra_ssr/src/replacing.rs | 6 +- crates/ra_ssr/src/tests.rs | 189 ++++++++--------------------------- crates/rust-analyzer/src/bin/args.rs | 36 ++++++- crates/rust-analyzer/src/bin/main.rs | 3 + crates/rust-analyzer/src/cli.rs | 2 +- crates/rust-analyzer/src/cli/ssr.rs | 40 +++++++- 8 files changed, 285 insertions(+), 163 deletions(-) diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs index e148f4564..422e15ee6 100644 --- a/crates/ra_ssr/src/lib.rs +++ b/crates/ra_ssr/src/lib.rs @@ -9,10 +9,11 @@ mod replacing; #[cfg(test)] mod tests; -use crate::matching::Match; +pub use crate::matching::Match; +use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason}; use hir::Semantics; use ra_db::{FileId, FileRange}; -use ra_syntax::{ast, AstNode, SmolStr, SyntaxNode}; +use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange}; use ra_text_edit::TextEdit; use rustc_hash::FxHashMap; @@ -26,7 +27,7 @@ pub struct SsrRule { } #[derive(Debug)] -struct SsrPattern { +pub struct SsrPattern { raw: parsing::RawSearchPattern, /// Placeholders keyed by the stand-in ident that we use in Rust source code. placeholders_by_stand_in: FxHashMap, @@ -45,7 +46,7 @@ pub struct SsrError(String); #[derive(Debug, Default)] pub struct SsrMatches { - matches: Vec, + pub matches: Vec, } /// Searches a crate for pattern matches and possibly replaces them with something else. @@ -64,6 +65,12 @@ impl<'db> MatchFinder<'db> { self.rules.push(rule); } + /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you + /// intend to do replacement, use `add_rule` instead. + pub fn add_search_pattern(&mut self, pattern: SsrPattern) { + self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) + } + pub fn edits_for_file(&self, file_id: FileId) -> Option { let matches = self.find_matches_in_file(file_id); if matches.matches.is_empty() { @@ -74,7 +81,7 @@ impl<'db> MatchFinder<'db> { } } - fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { + pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { let file = self.sema.parse(file_id); let code = file.syntax(); let mut matches = SsrMatches::default(); @@ -82,6 +89,32 @@ impl<'db> MatchFinder<'db> { matches } + /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match + /// them, while recording reasons why they don't match. This API is useful for command + /// line-based debugging where providing a range is difficult. + pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec { + use ra_db::SourceDatabaseExt; + let file = self.sema.parse(file_id); + let mut res = Vec::new(); + let file_text = self.sema.db.file_text(file_id); + let mut remaining_text = file_text.as_str(); + let mut base = 0; + let len = snippet.len() as u32; + while let Some(offset) = remaining_text.find(snippet) { + let start = base + offset as u32; + let end = start + len; + self.output_debug_for_nodes_at_range( + file.syntax(), + FileRange { file_id, range: TextRange::new(start.into(), end.into()) }, + &None, + &mut res, + ); + remaining_text = &remaining_text[offset + snippet.len()..]; + base = end; + } + res + } + fn find_matches( &self, code: &SyntaxNode, @@ -128,6 +161,59 @@ impl<'db> MatchFinder<'db> { self.find_matches(&child, restrict_range, matches_out); } } + + fn output_debug_for_nodes_at_range( + &self, + node: &SyntaxNode, + range: FileRange, + restrict_range: &Option, + out: &mut Vec, + ) { + for node in node.children() { + let node_range = self.sema.original_range(&node); + if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range) + { + continue; + } + if node_range.range == range.range { + for rule in &self.rules { + let pattern = + rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); + out.push(MatchDebugInfo { + matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) + .map_err(|e| MatchFailureReason { + reason: e.reason.unwrap_or_else(|| { + "Match failed, but no reason was given".to_owned() + }), + }), + pattern, + node: node.clone(), + }); + } + } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) { + if let Some(expanded) = self.sema.expand(¯o_call) { + if let Some(tt) = macro_call.token_tree() { + self.output_debug_for_nodes_at_range( + &expanded, + range, + &Some(self.sema.original_range(tt.syntax())), + out, + ); + } + } + } else { + self.output_debug_for_nodes_at_range(&node, range, restrict_range, out); + } + } + } +} + +pub struct MatchDebugInfo { + node: SyntaxNode, + /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, + /// etc. Will be absent if the pattern can't be parsed as that kind. + pattern: Result, + matched: Result, } impl std::fmt::Display for SsrError { @@ -136,4 +222,70 @@ impl std::fmt::Display for SsrError { } } +impl std::fmt::Debug for MatchDebugInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "========= PATTERN ==========\n")?; + match &self.pattern { + Ok(pattern) => { + write!(f, "{:#?}", pattern)?; + } + Err(err) => { + write!(f, "{}", err.reason)?; + } + } + write!( + f, + "\n============ AST ===========\n\ + {:#?}\n============================\n", + self.node + )?; + match &self.matched { + Ok(_) => write!(f, "Node matched")?, + Err(reason) => write!(f, "Node failed to match because: {}", reason.reason)?, + } + Ok(()) + } +} + +impl SsrPattern { + fn tree_for_kind_with_reason( + &self, + kind: SyntaxKind, + ) -> Result<&SyntaxNode, MatchFailureReason> { + record_match_fails_reasons_scope(true, || self.tree_for_kind(kind)) + .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() }) + } +} + +impl SsrMatches { + /// Returns `self` with any nested matches removed and made into top-level matches. + pub fn flattened(self) -> SsrMatches { + let mut out = SsrMatches::default(); + self.flatten_into(&mut out); + out + } + + fn flatten_into(self, out: &mut SsrMatches) { + for mut m in self.matches { + for p in m.placeholder_values.values_mut() { + std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out); + } + out.matches.push(m); + } + } +} + +impl Match { + pub fn matched_text(&self) -> String { + self.matched_node.text().to_string() + } +} + impl std::error::Error for SsrError {} + +#[cfg(test)] +impl MatchDebugInfo { + pub(crate) fn match_failure_reason(&self) -> Option<&str> { + self.matched.as_ref().err().map(|r| r.reason.as_str()) + } +} diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs index 54413a151..53d802e77 100644 --- a/crates/ra_ssr/src/matching.rs +++ b/crates/ra_ssr/src/matching.rs @@ -8,9 +8,7 @@ use crate::{ use hir::Semantics; use ra_db::FileRange; use ra_syntax::ast::{AstNode, AstToken}; -use ra_syntax::{ - ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, -}; +use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken}; use rustc_hash::FxHashMap; use std::{cell::Cell, iter::Peekable}; @@ -44,8 +42,8 @@ macro_rules! fail_match { /// Information about a match that was found. #[derive(Debug)] -pub(crate) struct Match { - pub(crate) range: TextRange, +pub struct Match { + pub(crate) range: FileRange, pub(crate) matched_node: SyntaxNode, pub(crate) placeholder_values: FxHashMap, pub(crate) ignored_comments: Vec, @@ -135,7 +133,7 @@ impl<'db, 'sema> MatchState<'db, 'sema> { match_state.attempt_match_node(&match_inputs, &pattern_tree, code)?; match_state.validate_range(&sema.original_range(code))?; match_state.match_out = Some(Match { - range: sema.original_range(code).range, + range: sema.original_range(code), matched_node: code.clone(), placeholder_values: FxHashMap::default(), ignored_comments: Vec::new(), diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs index 70ce1c185..e43cc5167 100644 --- a/crates/ra_ssr/src/replacing.rs +++ b/crates/ra_ssr/src/replacing.rs @@ -21,8 +21,10 @@ fn matches_to_edit_at_offset( ) -> TextEdit { let mut edit_builder = ra_text_edit::TextEditBuilder::default(); for m in &matches.matches { - edit_builder - .replace(m.range.checked_sub(relative_start).unwrap(), render_replace(m, file_src)); + edit_builder.replace( + m.range.range.checked_sub(relative_start).unwrap(), + render_replace(m, file_src), + ); } edit_builder.finish() } diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs index 57b2f50b2..c692c97e2 100644 --- a/crates/ra_ssr/src/tests.rs +++ b/crates/ra_ssr/src/tests.rs @@ -1,150 +1,5 @@ -use crate::matching::MatchFailureReason; -use crate::{matching, Match, MatchFinder, SsrMatches, SsrPattern, SsrRule}; -use matching::record_match_fails_reasons_scope; -use ra_db::{FileId, FileRange, SourceDatabaseExt}; -use ra_syntax::ast::AstNode; -use ra_syntax::{ast, SyntaxKind, SyntaxNode, TextRange}; - -struct MatchDebugInfo { - node: SyntaxNode, - /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, - /// etc. Will be absent if the pattern can't be parsed as that kind. - pattern: Result, - matched: Result, -} - -impl SsrPattern { - pub(crate) fn tree_for_kind_with_reason( - &self, - kind: SyntaxKind, - ) -> Result<&SyntaxNode, MatchFailureReason> { - record_match_fails_reasons_scope(true, || self.tree_for_kind(kind)) - .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() }) - } -} - -impl std::fmt::Debug for MatchDebugInfo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "========= PATTERN ==========\n")?; - match &self.pattern { - Ok(pattern) => { - write!(f, "{:#?}", pattern)?; - } - Err(err) => { - write!(f, "{}", err.reason)?; - } - } - write!( - f, - "\n============ AST ===========\n\ - {:#?}\n============================", - self.node - )?; - match &self.matched { - Ok(_) => write!(f, "Node matched")?, - Err(reason) => write!(f, "Node failed to match because: {}", reason.reason)?, - } - Ok(()) - } -} - -impl SsrMatches { - /// Returns `self` with any nested matches removed and made into top-level matches. - pub(crate) fn flattened(self) -> SsrMatches { - let mut out = SsrMatches::default(); - self.flatten_into(&mut out); - out - } - - fn flatten_into(self, out: &mut SsrMatches) { - for mut m in self.matches { - for p in m.placeholder_values.values_mut() { - std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out); - } - out.matches.push(m); - } - } -} - -impl Match { - pub(crate) fn matched_text(&self) -> String { - self.matched_node.text().to_string() - } -} - -impl<'db> MatchFinder<'db> { - /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you - /// intend to do replacement, use `add_rule` instead. - fn add_search_pattern(&mut self, pattern: SsrPattern) { - self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) - } - - /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match - /// them, while recording reasons why they don't match. This API is useful for command - /// line-based debugging where providing a range is difficult. - fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec { - let file = self.sema.parse(file_id); - let mut res = Vec::new(); - let file_text = self.sema.db.file_text(file_id); - let mut remaining_text = file_text.as_str(); - let mut base = 0; - let len = snippet.len() as u32; - while let Some(offset) = remaining_text.find(snippet) { - let start = base + offset as u32; - let end = start + len; - self.output_debug_for_nodes_at_range( - file.syntax(), - TextRange::new(start.into(), end.into()), - &None, - &mut res, - ); - remaining_text = &remaining_text[offset + snippet.len()..]; - base = end; - } - res - } - - fn output_debug_for_nodes_at_range( - &self, - node: &SyntaxNode, - range: TextRange, - restrict_range: &Option, - out: &mut Vec, - ) { - for node in node.children() { - if !node.text_range().contains_range(range) { - continue; - } - if node.text_range() == range { - for rule in &self.rules { - let pattern = - rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); - out.push(MatchDebugInfo { - matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) - .map_err(|e| MatchFailureReason { - reason: e.reason.unwrap_or_else(|| { - "Match failed, but no reason was given".to_owned() - }), - }), - pattern, - node: node.clone(), - }); - } - } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) { - if let Some(expanded) = self.sema.expand(¯o_call) { - if let Some(tt) = macro_call.token_tree() { - self.output_debug_for_nodes_at_range( - &expanded, - range, - &Some(self.sema.original_range(tt.syntax())), - out, - ); - } - } - } - } - } -} +use crate::{MatchFinder, SsrRule}; +use ra_db::{FileId, SourceDatabaseExt}; fn parse_error_text(query: &str) -> String { format!("{}", query.parse::().unwrap_err()) @@ -260,6 +115,19 @@ fn assert_no_match(pattern: &str, code: &str) { assert_matches(pattern, code, &[]); } +fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { + let (db, file_id) = single_file(code); + let mut match_finder = MatchFinder::new(&db); + match_finder.add_search_pattern(pattern.parse().unwrap()); + let mut reasons = Vec::new(); + for d in match_finder.debug_where_text_equal(file_id, snippet) { + if let Some(reason) = d.match_failure_reason() { + reasons.push(reason.to_owned()); + } + } + assert_eq!(reasons, vec![expected_reason]); +} + #[test] fn ssr_function_to_method() { assert_ssr_transform( @@ -623,3 +491,30 @@ fn preserves_whitespace_within_macro_expansion() { fn f() {macro1!(4 - 3 - 1 * 2}"#, ) } + +#[test] +fn match_failure_reasons() { + let code = r#" + macro_rules! foo { + ($a:expr) => { + 1 + $a + 2 + }; + } + fn f1() { + bar(1, 2); + foo!(5 + 43.to_string() + 5); + } + "#; + assert_match_failure_reason( + "bar($a, 3)", + code, + "bar(1, 2)", + r#"Pattern wanted token '3' (INT_NUMBER), but code had token '2' (INT_NUMBER)"#, + ); + assert_match_failure_reason( + "42.to_string()", + code, + "43.to_string()", + r#"Pattern wanted token '42' (INT_NUMBER), but code had token '43' (INT_NUMBER)"#, + ); +} diff --git a/crates/rust-analyzer/src/bin/args.rs b/crates/rust-analyzer/src/bin/args.rs index 8a0b10117..8c0f4df8b 100644 --- a/crates/rust-analyzer/src/bin/args.rs +++ b/crates/rust-analyzer/src/bin/args.rs @@ -5,7 +5,7 @@ use anyhow::{bail, Result}; use pico_args::Arguments; -use ra_ssr::SsrRule; +use ra_ssr::{SsrPattern, SsrRule}; use rust_analyzer::cli::{BenchWhat, Position, Verbosity}; use std::{fmt::Write, path::PathBuf}; @@ -50,6 +50,10 @@ pub(crate) enum Command { Ssr { rules: Vec, }, + StructuredSearch { + debug_snippet: Option, + patterns: Vec, + }, ProcMacro, RunServer, Version, @@ -294,6 +298,7 @@ EXAMPLE: rust-analyzer ssr '$a.foo($b) ==> bar($a, $b)' FLAGS: + --debug Prints debug information for any nodes with source exactly equal to -h, --help Prints help information ARGS: @@ -307,6 +312,34 @@ ARGS: } Command::Ssr { rules } } + "search" => { + if matches.contains(["-h", "--help"]) { + eprintln!( + "\ +rust-analyzer search + +USAGE: + rust-analyzer search [FLAGS] [PATTERN...] + +EXAMPLE: + rust-analyzer search '$a.foo($b)' + +FLAGS: + --debug Prints debug information for any nodes with source exactly equal to + -h, --help Prints help information + +ARGS: + A structured search pattern" + ); + return Ok(Err(HelpPrinted)); + } + let debug_snippet = matches.opt_value_from_str("--debug")?; + let mut patterns = Vec::new(); + while let Some(rule) = matches.free_from_str()? { + patterns.push(rule); + } + Command::StructuredSearch { patterns, debug_snippet } + } _ => { print_subcommands(); return Ok(Err(HelpPrinted)); @@ -334,6 +367,7 @@ SUBCOMMANDS: diagnostics proc-macro parse + search ssr symbols" ) diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 0f55c3ee2..eec76d415 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -65,6 +65,9 @@ fn main() -> Result<()> { args::Command::Ssr { rules } => { cli::apply_ssr_rules(rules)?; } + args::Command::StructuredSearch { patterns, debug_snippet } => { + cli::search_for_patterns(patterns, debug_snippet)?; + } args::Command::Version => println!("rust-analyzer {}", env!("REV")), } Ok(()) diff --git a/crates/rust-analyzer/src/cli.rs b/crates/rust-analyzer/src/cli.rs index 13e3d75be..6863f100b 100644 --- a/crates/rust-analyzer/src/cli.rs +++ b/crates/rust-analyzer/src/cli.rs @@ -18,7 +18,7 @@ pub use analysis_bench::{analysis_bench, BenchWhat, Position}; pub use analysis_stats::analysis_stats; pub use diagnostics::diagnostics; pub use load_cargo::load_cargo; -pub use ssr::apply_ssr_rules; +pub use ssr::{apply_ssr_rules, search_for_patterns}; #[derive(Clone, Copy)] pub enum Verbosity { diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs index a5265ac15..4fb829ea5 100644 --- a/crates/rust-analyzer/src/cli/ssr.rs +++ b/crates/rust-analyzer/src/cli/ssr.rs @@ -2,7 +2,7 @@ use crate::cli::{load_cargo::load_cargo, Result}; use ra_ide::SourceFileEdit; -use ra_ssr::{MatchFinder, SsrRule}; +use ra_ssr::{MatchFinder, SsrPattern, SsrRule}; pub fn apply_ssr_rules(rules: Vec) -> Result<()> { use ra_db::SourceDatabaseExt; @@ -31,3 +31,41 @@ pub fn apply_ssr_rules(rules: Vec) -> Result<()> { } Ok(()) } + +/// Searches for `patterns`, printing debug information for any nodes whose text exactly matches +/// `debug_snippet`. This is intended for debugging and probably isn't in it's current form useful +/// for much else. +pub fn search_for_patterns(patterns: Vec, debug_snippet: Option) -> Result<()> { + use ra_db::SourceDatabaseExt; + use ra_ide_db::symbol_index::SymbolsDatabase; + let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?; + let db = host.raw_database(); + let mut match_finder = MatchFinder::new(db); + for pattern in patterns { + match_finder.add_search_pattern(pattern); + } + for &root in db.local_roots().iter() { + let sr = db.source_root(root); + for file_id in sr.iter() { + if let Some(debug_snippet) = &debug_snippet { + for debug_info in match_finder.debug_where_text_equal(file_id, debug_snippet) { + println!("{:#?}", debug_info); + } + } else { + let matches = match_finder.find_matches_in_file(file_id); + if !matches.matches.is_empty() { + let matches = matches.flattened().matches; + if let Some(path) = vfs.file_path(file_id).as_path() { + println!("{} matches in '{}'", matches.len(), path.to_string_lossy()); + } + // We could possibly at some point do something more useful than just printing + // the matched text. For now though, that's the easiest thing to do. + for m in matches { + println!("{}", m.matched_text()); + } + } + } + } + } + Ok(()) +} -- cgit v1.2.3