Skip to content

Commit dd3ad2b

Browse files
bors[bot]davidlattimorelnicola
authored
Merge #5154 #5157
5154: Structured search debugging r=matklad a=davidlattimore Adds a "search" mode to the rust-analyzer binary that does structured search (SSR without the replace part). This is intended primarily for debugging why a bit of code isn't matching a pattern. 5157: Use dynamic dispatch in AstDiagnostic r=matklad a=lnicola Co-authored-by: David Lattimore <[email protected]> Co-authored-by: Laurențiu Nicola <[email protected]>
3 parents 686e115 + 95f8310 + 1be5e84 commit dd3ad2b

File tree

10 files changed

+291
-169
lines changed

10 files changed

+291
-169
lines changed

crates/ra_hir_expand/src/diagnostics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub trait Diagnostic: Any + Send + Sync + fmt::Debug + 'static {
2828

2929
pub trait AstDiagnostic {
3030
type AST;
31-
fn ast(&self, db: &impl AstDatabase) -> Self::AST;
31+
fn ast(&self, db: &dyn AstDatabase) -> Self::AST;
3232
}
3333

3434
impl dyn Diagnostic {

crates/ra_hir_ty/src/diagnostics.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl Diagnostic for NoSuchField {
3232
impl AstDiagnostic for NoSuchField {
3333
type AST = ast::RecordField;
3434

35-
fn ast(&self, db: &impl AstDatabase) -> Self::AST {
35+
fn ast(&self, db: &dyn AstDatabase) -> Self::AST {
3636
let root = db.parse_or_expand(self.source().file_id).unwrap();
3737
let node = self.source().value.to_node(&root);
3838
ast::RecordField::cast(node).unwrap()
@@ -65,7 +65,7 @@ impl Diagnostic for MissingFields {
6565
impl AstDiagnostic for MissingFields {
6666
type AST = ast::RecordFieldList;
6767

68-
fn ast(&self, db: &impl AstDatabase) -> Self::AST {
68+
fn ast(&self, db: &dyn AstDatabase) -> Self::AST {
6969
let root = db.parse_or_expand(self.source().file_id).unwrap();
7070
let node = self.source().value.to_node(&root);
7171
ast::RecordFieldList::cast(node).unwrap()
@@ -135,7 +135,7 @@ impl Diagnostic for MissingOkInTailExpr {
135135
impl AstDiagnostic for MissingOkInTailExpr {
136136
type AST = ast::Expr;
137137

138-
fn ast(&self, db: &impl AstDatabase) -> Self::AST {
138+
fn ast(&self, db: &dyn AstDatabase) -> Self::AST {
139139
let root = db.parse_or_expand(self.file).unwrap();
140140
let node = self.source().value.to_node(&root);
141141
ast::Expr::cast(node).unwrap()
@@ -163,7 +163,7 @@ impl Diagnostic for BreakOutsideOfLoop {
163163
impl AstDiagnostic for BreakOutsideOfLoop {
164164
type AST = ast::Expr;
165165

166-
fn ast(&self, db: &impl AstDatabase) -> Self::AST {
166+
fn ast(&self, db: &dyn AstDatabase) -> Self::AST {
167167
let root = db.parse_or_expand(self.file).unwrap();
168168
let node = self.source().value.to_node(&root);
169169
ast::Expr::cast(node).unwrap()
@@ -191,7 +191,7 @@ impl Diagnostic for MissingUnsafe {
191191
impl AstDiagnostic for MissingUnsafe {
192192
type AST = ast::Expr;
193193

194-
fn ast(&self, db: &impl AstDatabase) -> Self::AST {
194+
fn ast(&self, db: &dyn AstDatabase) -> Self::AST {
195195
let root = db.parse_or_expand(self.source().file_id).unwrap();
196196
let node = self.source().value.to_node(&root);
197197
ast::Expr::cast(node).unwrap()

crates/ra_ssr/src/lib.rs

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ mod replacing;
99
#[cfg(test)]
1010
mod tests;
1111

12-
use crate::matching::Match;
12+
pub use crate::matching::Match;
13+
use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason};
1314
use hir::Semantics;
1415
use ra_db::{FileId, FileRange};
15-
use ra_syntax::{ast, AstNode, SmolStr, SyntaxNode};
16+
use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange};
1617
use ra_text_edit::TextEdit;
1718
use rustc_hash::FxHashMap;
1819

@@ -26,7 +27,7 @@ pub struct SsrRule {
2627
}
2728

2829
#[derive(Debug)]
29-
struct SsrPattern {
30+
pub struct SsrPattern {
3031
raw: parsing::RawSearchPattern,
3132
/// Placeholders keyed by the stand-in ident that we use in Rust source code.
3233
placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>,
@@ -45,7 +46,7 @@ pub struct SsrError(String);
4546

4647
#[derive(Debug, Default)]
4748
pub struct SsrMatches {
48-
matches: Vec<Match>,
49+
pub matches: Vec<Match>,
4950
}
5051

5152
/// Searches a crate for pattern matches and possibly replaces them with something else.
@@ -64,6 +65,12 @@ impl<'db> MatchFinder<'db> {
6465
self.rules.push(rule);
6566
}
6667

68+
/// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
69+
/// intend to do replacement, use `add_rule` instead.
70+
pub fn add_search_pattern(&mut self, pattern: SsrPattern) {
71+
self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() })
72+
}
73+
6774
pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> {
6875
let matches = self.find_matches_in_file(file_id);
6976
if matches.matches.is_empty() {
@@ -74,14 +81,40 @@ impl<'db> MatchFinder<'db> {
7481
}
7582
}
7683

77-
fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches {
84+
pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches {
7885
let file = self.sema.parse(file_id);
7986
let code = file.syntax();
8087
let mut matches = SsrMatches::default();
8188
self.find_matches(code, &None, &mut matches);
8289
matches
8390
}
8491

92+
/// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
93+
/// them, while recording reasons why they don't match. This API is useful for command
94+
/// line-based debugging where providing a range is difficult.
95+
pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> {
96+
use ra_db::SourceDatabaseExt;
97+
let file = self.sema.parse(file_id);
98+
let mut res = Vec::new();
99+
let file_text = self.sema.db.file_text(file_id);
100+
let mut remaining_text = file_text.as_str();
101+
let mut base = 0;
102+
let len = snippet.len() as u32;
103+
while let Some(offset) = remaining_text.find(snippet) {
104+
let start = base + offset as u32;
105+
let end = start + len;
106+
self.output_debug_for_nodes_at_range(
107+
file.syntax(),
108+
FileRange { file_id, range: TextRange::new(start.into(), end.into()) },
109+
&None,
110+
&mut res,
111+
);
112+
remaining_text = &remaining_text[offset + snippet.len()..];
113+
base = end;
114+
}
115+
res
116+
}
117+
85118
fn find_matches(
86119
&self,
87120
code: &SyntaxNode,
@@ -128,6 +161,59 @@ impl<'db> MatchFinder<'db> {
128161
self.find_matches(&child, restrict_range, matches_out);
129162
}
130163
}
164+
165+
fn output_debug_for_nodes_at_range(
166+
&self,
167+
node: &SyntaxNode,
168+
range: FileRange,
169+
restrict_range: &Option<FileRange>,
170+
out: &mut Vec<MatchDebugInfo>,
171+
) {
172+
for node in node.children() {
173+
let node_range = self.sema.original_range(&node);
174+
if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
175+
{
176+
continue;
177+
}
178+
if node_range.range == range.range {
179+
for rule in &self.rules {
180+
let pattern =
181+
rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone());
182+
out.push(MatchDebugInfo {
183+
matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
184+
.map_err(|e| MatchFailureReason {
185+
reason: e.reason.unwrap_or_else(|| {
186+
"Match failed, but no reason was given".to_owned()
187+
}),
188+
}),
189+
pattern,
190+
node: node.clone(),
191+
});
192+
}
193+
} else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
194+
if let Some(expanded) = self.sema.expand(&macro_call) {
195+
if let Some(tt) = macro_call.token_tree() {
196+
self.output_debug_for_nodes_at_range(
197+
&expanded,
198+
range,
199+
&Some(self.sema.original_range(tt.syntax())),
200+
out,
201+
);
202+
}
203+
}
204+
} else {
205+
self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
206+
}
207+
}
208+
}
209+
}
210+
211+
pub struct MatchDebugInfo {
212+
node: SyntaxNode,
213+
/// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item,
214+
/// etc. Will be absent if the pattern can't be parsed as that kind.
215+
pattern: Result<SyntaxNode, MatchFailureReason>,
216+
matched: Result<Match, MatchFailureReason>,
131217
}
132218

133219
impl std::fmt::Display for SsrError {
@@ -136,4 +222,70 @@ impl std::fmt::Display for SsrError {
136222
}
137223
}
138224

225+
impl std::fmt::Debug for MatchDebugInfo {
226+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227+
write!(f, "========= PATTERN ==========\n")?;
228+
match &self.pattern {
229+
Ok(pattern) => {
230+
write!(f, "{:#?}", pattern)?;
231+
}
232+
Err(err) => {
233+
write!(f, "{}", err.reason)?;
234+
}
235+
}
236+
write!(
237+
f,
238+
"\n============ AST ===========\n\
239+
{:#?}\n============================\n",
240+
self.node
241+
)?;
242+
match &self.matched {
243+
Ok(_) => write!(f, "Node matched")?,
244+
Err(reason) => write!(f, "Node failed to match because: {}", reason.reason)?,
245+
}
246+
Ok(())
247+
}
248+
}
249+
250+
impl SsrPattern {
251+
fn tree_for_kind_with_reason(
252+
&self,
253+
kind: SyntaxKind,
254+
) -> Result<&SyntaxNode, MatchFailureReason> {
255+
record_match_fails_reasons_scope(true, || self.tree_for_kind(kind))
256+
.map_err(|e| MatchFailureReason { reason: e.reason.unwrap() })
257+
}
258+
}
259+
260+
impl SsrMatches {
261+
/// Returns `self` with any nested matches removed and made into top-level matches.
262+
pub fn flattened(self) -> SsrMatches {
263+
let mut out = SsrMatches::default();
264+
self.flatten_into(&mut out);
265+
out
266+
}
267+
268+
fn flatten_into(self, out: &mut SsrMatches) {
269+
for mut m in self.matches {
270+
for p in m.placeholder_values.values_mut() {
271+
std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out);
272+
}
273+
out.matches.push(m);
274+
}
275+
}
276+
}
277+
278+
impl Match {
279+
pub fn matched_text(&self) -> String {
280+
self.matched_node.text().to_string()
281+
}
282+
}
283+
139284
impl std::error::Error for SsrError {}
285+
286+
#[cfg(test)]
287+
impl MatchDebugInfo {
288+
pub(crate) fn match_failure_reason(&self) -> Option<&str> {
289+
self.matched.as_ref().err().map(|r| r.reason.as_str())
290+
}
291+
}

crates/ra_ssr/src/matching.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ use crate::{
88
use hir::Semantics;
99
use ra_db::FileRange;
1010
use ra_syntax::ast::{AstNode, AstToken};
11-
use ra_syntax::{
12-
ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken, TextRange,
13-
};
11+
use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken};
1412
use rustc_hash::FxHashMap;
1513
use std::{cell::Cell, iter::Peekable};
1614

@@ -44,8 +42,8 @@ macro_rules! fail_match {
4442

4543
/// Information about a match that was found.
4644
#[derive(Debug)]
47-
pub(crate) struct Match {
48-
pub(crate) range: TextRange,
45+
pub struct Match {
46+
pub(crate) range: FileRange,
4947
pub(crate) matched_node: SyntaxNode,
5048
pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>,
5149
pub(crate) ignored_comments: Vec<ast::Comment>,
@@ -135,7 +133,7 @@ impl<'db, 'sema> MatchState<'db, 'sema> {
135133
match_state.attempt_match_node(&match_inputs, &pattern_tree, code)?;
136134
match_state.validate_range(&sema.original_range(code))?;
137135
match_state.match_out = Some(Match {
138-
range: sema.original_range(code).range,
136+
range: sema.original_range(code),
139137
matched_node: code.clone(),
140138
placeholder_values: FxHashMap::default(),
141139
ignored_comments: Vec::new(),

crates/ra_ssr/src/replacing.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ fn matches_to_edit_at_offset(
2121
) -> TextEdit {
2222
let mut edit_builder = ra_text_edit::TextEditBuilder::default();
2323
for m in &matches.matches {
24-
edit_builder
25-
.replace(m.range.checked_sub(relative_start).unwrap(), render_replace(m, file_src));
24+
edit_builder.replace(
25+
m.range.range.checked_sub(relative_start).unwrap(),
26+
render_replace(m, file_src),
27+
);
2628
}
2729
edit_builder.finish()
2830
}

0 commit comments

Comments
 (0)