diff options
-rw-r--r-- | crates/ra_hir_expand/src/diagnostics.rs | 2 | ||||
-rw-r--r-- | crates/ra_hir_ty/src/diagnostics.rs | 10 | ||||
-rw-r--r-- | crates/ra_ssr/src/lib.rs | 162 | ||||
-rw-r--r-- | crates/ra_ssr/src/matching.rs | 10 | ||||
-rw-r--r-- | crates/ra_ssr/src/replacing.rs | 6 | ||||
-rw-r--r-- | crates/ra_ssr/src/tests.rs | 189 | ||||
-rw-r--r-- | crates/rust-analyzer/src/bin/args.rs | 36 | ||||
-rw-r--r-- | crates/rust-analyzer/src/bin/main.rs | 3 | ||||
-rw-r--r-- | crates/rust-analyzer/src/cli.rs | 2 | ||||
-rw-r--r-- | crates/rust-analyzer/src/cli/ssr.rs | 40 |
10 files changed, 291 insertions, 169 deletions
diff --git a/crates/ra_hir_expand/src/diagnostics.rs b/crates/ra_hir_expand/src/diagnostics.rs index 99209c6e8..545cff9bd 100644 --- a/crates/ra_hir_expand/src/diagnostics.rs +++ b/crates/ra_hir_expand/src/diagnostics.rs | |||
@@ -28,7 +28,7 @@ pub trait Diagnostic: Any + Send + Sync + fmt::Debug + 'static { | |||
28 | 28 | ||
29 | pub trait AstDiagnostic { | 29 | pub trait AstDiagnostic { |
30 | type AST; | 30 | type AST; |
31 | fn ast(&self, db: &impl AstDatabase) -> Self::AST; | 31 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST; |
32 | } | 32 | } |
33 | 33 | ||
34 | impl dyn Diagnostic { | 34 | impl dyn Diagnostic { |
diff --git a/crates/ra_hir_ty/src/diagnostics.rs b/crates/ra_hir_ty/src/diagnostics.rs index a59efb347..0289911de 100644 --- a/crates/ra_hir_ty/src/diagnostics.rs +++ b/crates/ra_hir_ty/src/diagnostics.rs | |||
@@ -32,7 +32,7 @@ impl Diagnostic for NoSuchField { | |||
32 | impl AstDiagnostic for NoSuchField { | 32 | impl AstDiagnostic for NoSuchField { |
33 | type AST = ast::RecordField; | 33 | type AST = ast::RecordField; |
34 | 34 | ||
35 | fn ast(&self, db: &impl AstDatabase) -> Self::AST { | 35 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { |
36 | let root = db.parse_or_expand(self.source().file_id).unwrap(); | 36 | let root = db.parse_or_expand(self.source().file_id).unwrap(); |
37 | let node = self.source().value.to_node(&root); | 37 | let node = self.source().value.to_node(&root); |
38 | ast::RecordField::cast(node).unwrap() | 38 | ast::RecordField::cast(node).unwrap() |
@@ -65,7 +65,7 @@ impl Diagnostic for MissingFields { | |||
65 | impl AstDiagnostic for MissingFields { | 65 | impl AstDiagnostic for MissingFields { |
66 | type AST = ast::RecordFieldList; | 66 | type AST = ast::RecordFieldList; |
67 | 67 | ||
68 | fn ast(&self, db: &impl AstDatabase) -> Self::AST { | 68 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { |
69 | let root = db.parse_or_expand(self.source().file_id).unwrap(); | 69 | let root = db.parse_or_expand(self.source().file_id).unwrap(); |
70 | let node = self.source().value.to_node(&root); | 70 | let node = self.source().value.to_node(&root); |
71 | ast::RecordFieldList::cast(node).unwrap() | 71 | ast::RecordFieldList::cast(node).unwrap() |
@@ -135,7 +135,7 @@ impl Diagnostic for MissingOkInTailExpr { | |||
135 | impl AstDiagnostic for MissingOkInTailExpr { | 135 | impl AstDiagnostic for MissingOkInTailExpr { |
136 | type AST = ast::Expr; | 136 | type AST = ast::Expr; |
137 | 137 | ||
138 | fn ast(&self, db: &impl AstDatabase) -> Self::AST { | 138 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { |
139 | let root = db.parse_or_expand(self.file).unwrap(); | 139 | let root = db.parse_or_expand(self.file).unwrap(); |
140 | let node = self.source().value.to_node(&root); | 140 | let node = self.source().value.to_node(&root); |
141 | ast::Expr::cast(node).unwrap() | 141 | ast::Expr::cast(node).unwrap() |
@@ -163,7 +163,7 @@ impl Diagnostic for BreakOutsideOfLoop { | |||
163 | impl AstDiagnostic for BreakOutsideOfLoop { | 163 | impl AstDiagnostic for BreakOutsideOfLoop { |
164 | type AST = ast::Expr; | 164 | type AST = ast::Expr; |
165 | 165 | ||
166 | fn ast(&self, db: &impl AstDatabase) -> Self::AST { | 166 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { |
167 | let root = db.parse_or_expand(self.file).unwrap(); | 167 | let root = db.parse_or_expand(self.file).unwrap(); |
168 | let node = self.source().value.to_node(&root); | 168 | let node = self.source().value.to_node(&root); |
169 | ast::Expr::cast(node).unwrap() | 169 | ast::Expr::cast(node).unwrap() |
@@ -191,7 +191,7 @@ impl Diagnostic for MissingUnsafe { | |||
191 | impl AstDiagnostic for MissingUnsafe { | 191 | impl AstDiagnostic for MissingUnsafe { |
192 | type AST = ast::Expr; | 192 | type AST = ast::Expr; |
193 | 193 | ||
194 | fn ast(&self, db: &impl AstDatabase) -> Self::AST { | 194 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { |
195 | let root = db.parse_or_expand(self.source().file_id).unwrap(); | 195 | let root = db.parse_or_expand(self.source().file_id).unwrap(); |
196 | let node = self.source().value.to_node(&root); | 196 | let node = self.source().value.to_node(&root); |
197 | ast::Expr::cast(node).unwrap() | 197 | ast::Expr::cast(node).unwrap() |
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; | |||
9 | #[cfg(test)] | 9 | #[cfg(test)] |
10 | mod tests; | 10 | mod tests; |
11 | 11 | ||
12 | use crate::matching::Match; | 12 | pub use crate::matching::Match; |
13 | use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason}; | ||
13 | use hir::Semantics; | 14 | use hir::Semantics; |
14 | use ra_db::{FileId, FileRange}; | 15 | use ra_db::{FileId, FileRange}; |
15 | use ra_syntax::{ast, AstNode, SmolStr, SyntaxNode}; | 16 | use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange}; |
16 | use ra_text_edit::TextEdit; | 17 | use ra_text_edit::TextEdit; |
17 | use rustc_hash::FxHashMap; | 18 | use rustc_hash::FxHashMap; |
18 | 19 | ||
@@ -26,7 +27,7 @@ pub struct SsrRule { | |||
26 | } | 27 | } |
27 | 28 | ||
28 | #[derive(Debug)] | 29 | #[derive(Debug)] |
29 | struct SsrPattern { | 30 | pub struct SsrPattern { |
30 | raw: parsing::RawSearchPattern, | 31 | raw: parsing::RawSearchPattern, |
31 | /// Placeholders keyed by the stand-in ident that we use in Rust source code. | 32 | /// Placeholders keyed by the stand-in ident that we use in Rust source code. |
32 | placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>, | 33 | placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>, |
@@ -45,7 +46,7 @@ pub struct SsrError(String); | |||
45 | 46 | ||
46 | #[derive(Debug, Default)] | 47 | #[derive(Debug, Default)] |
47 | pub struct SsrMatches { | 48 | pub struct SsrMatches { |
48 | matches: Vec<Match>, | 49 | pub matches: Vec<Match>, |
49 | } | 50 | } |
50 | 51 | ||
51 | /// Searches a crate for pattern matches and possibly replaces them with something else. | 52 | /// Searches a crate for pattern matches and possibly replaces them with something else. |
@@ -64,6 +65,12 @@ impl<'db> MatchFinder<'db> { | |||
64 | self.rules.push(rule); | 65 | self.rules.push(rule); |
65 | } | 66 | } |
66 | 67 | ||
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 | |||
67 | pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { | 74 | pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { |
68 | let matches = self.find_matches_in_file(file_id); | 75 | let matches = self.find_matches_in_file(file_id); |
69 | if matches.matches.is_empty() { | 76 | if matches.matches.is_empty() { |
@@ -74,7 +81,7 @@ impl<'db> MatchFinder<'db> { | |||
74 | } | 81 | } |
75 | } | 82 | } |
76 | 83 | ||
77 | fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { | 84 | pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { |
78 | let file = self.sema.parse(file_id); | 85 | let file = self.sema.parse(file_id); |
79 | let code = file.syntax(); | 86 | let code = file.syntax(); |
80 | let mut matches = SsrMatches::default(); | 87 | let mut matches = SsrMatches::default(); |
@@ -82,6 +89,32 @@ impl<'db> MatchFinder<'db> { | |||
82 | matches | 89 | matches |
83 | } | 90 | } |
84 | 91 | ||
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 | |||
85 | fn find_matches( | 118 | fn find_matches( |
86 | &self, | 119 | &self, |
87 | code: &SyntaxNode, | 120 | code: &SyntaxNode, |
@@ -128,6 +161,59 @@ impl<'db> MatchFinder<'db> { | |||
128 | self.find_matches(&child, restrict_range, matches_out); | 161 | self.find_matches(&child, restrict_range, matches_out); |
129 | } | 162 | } |
130 | } | 163 | } |
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(¯o_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>, | ||
131 | } | 217 | } |
132 | 218 | ||
133 | impl std::fmt::Display for SsrError { | 219 | impl std::fmt::Display for SsrError { |
@@ -136,4 +222,70 @@ impl std::fmt::Display for SsrError { | |||
136 | } | 222 | } |
137 | } | 223 | } |
138 | 224 | ||
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 | |||
139 | impl std::error::Error for SsrError {} | 284 | 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 | } | ||
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::{ | |||
8 | use hir::Semantics; | 8 | use hir::Semantics; |
9 | use ra_db::FileRange; | 9 | use ra_db::FileRange; |
10 | use ra_syntax::ast::{AstNode, AstToken}; | 10 | use ra_syntax::ast::{AstNode, AstToken}; |
11 | use ra_syntax::{ | 11 | use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken}; |
12 | ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, | ||
13 | }; | ||
14 | use rustc_hash::FxHashMap; | 12 | use rustc_hash::FxHashMap; |
15 | use std::{cell::Cell, iter::Peekable}; | 13 | use std::{cell::Cell, iter::Peekable}; |
16 | 14 | ||
@@ -44,8 +42,8 @@ macro_rules! fail_match { | |||
44 | 42 | ||
45 | /// Information about a match that was found. | 43 | /// Information about a match that was found. |
46 | #[derive(Debug)] | 44 | #[derive(Debug)] |
47 | pub(crate) struct Match { | 45 | pub struct Match { |
48 | pub(crate) range: TextRange, | 46 | pub(crate) range: FileRange, |
49 | pub(crate) matched_node: SyntaxNode, | 47 | pub(crate) matched_node: SyntaxNode, |
50 | pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>, | 48 | pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>, |
51 | pub(crate) ignored_comments: Vec<ast::Comment>, | 49 | pub(crate) ignored_comments: Vec<ast::Comment>, |
@@ -135,7 +133,7 @@ impl<'db, 'sema> MatchState<'db, 'sema> { | |||
135 | match_state.attempt_match_node(&match_inputs, &pattern_tree, code)?; | 133 | match_state.attempt_match_node(&match_inputs, &pattern_tree, code)?; |
136 | match_state.validate_range(&sema.original_range(code))?; | 134 | match_state.validate_range(&sema.original_range(code))?; |
137 | match_state.match_out = Some(Match { | 135 | match_state.match_out = Some(Match { |
138 | range: sema.original_range(code).range, | 136 | range: sema.original_range(code), |
139 | matched_node: code.clone(), | 137 | matched_node: code.clone(), |
140 | placeholder_values: FxHashMap::default(), | 138 | placeholder_values: FxHashMap::default(), |
141 | ignored_comments: Vec::new(), | 139 | 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( | |||
21 | ) -> TextEdit { | 21 | ) -> TextEdit { |
22 | let mut edit_builder = ra_text_edit::TextEditBuilder::default(); | 22 | let mut edit_builder = ra_text_edit::TextEditBuilder::default(); |
23 | for m in &matches.matches { | 23 | for m in &matches.matches { |
24 | edit_builder | 24 | edit_builder.replace( |
25 | .replace(m.range.checked_sub(relative_start).unwrap(), render_replace(m, file_src)); | 25 | m.range.range.checked_sub(relative_start).unwrap(), |
26 | render_replace(m, file_src), | ||
27 | ); | ||
26 | } | 28 | } |
27 | edit_builder.finish() | 29 | edit_builder.finish() |
28 | } | 30 | } |
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 @@ | |||
1 | use crate::matching::MatchFailureReason; | 1 | use crate::{MatchFinder, SsrRule}; |
2 | use crate::{matching, Match, MatchFinder, SsrMatches, SsrPattern, SsrRule}; | 2 | use ra_db::{FileId, SourceDatabaseExt}; |
3 | use matching::record_match_fails_reasons_scope; | ||
4 | use ra_db::{FileId, FileRange, SourceDatabaseExt}; | ||
5 | use ra_syntax::ast::AstNode; | ||
6 | use ra_syntax::{ast, SyntaxKind, SyntaxNode, TextRange}; | ||
7 | |||
8 | struct MatchDebugInfo { | ||
9 | node: SyntaxNode, | ||
10 | /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, | ||
11 | /// etc. Will be absent if the pattern can't be parsed as that kind. | ||
12 | pattern: Result<SyntaxNode, MatchFailureReason>, | ||
13 | matched: Result<Match, MatchFailureReason>, | ||
14 | } | ||
15 | |||
16 | impl SsrPattern { | ||
17 | pub(crate) fn tree_for_kind_with_reason( | ||
18 | &self, | ||
19 | kind: SyntaxKind, | ||
20 | ) -> Result<&SyntaxNode, MatchFailureReason> { | ||
21 | record_match_fails_reasons_scope(true, || self.tree_for_kind(kind)) | ||
22 | .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() }) | ||
23 | } | ||
24 | } | ||
25 | |||
26 | impl std::fmt::Debug for MatchDebugInfo { | ||
27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
28 | write!(f, "========= PATTERN ==========\n")?; | ||
29 | match &self.pattern { | ||
30 | Ok(pattern) => { | ||
31 | write!(f, "{:#?}", pattern)?; | ||
32 | } | ||
33 | Err(err) => { | ||
34 | write!(f, "{}", err.reason)?; | ||
35 | } | ||
36 | } | ||
37 | write!( | ||
38 | f, | ||
39 | "\n============ AST ===========\n\ | ||
40 | {:#?}\n============================", | ||
41 | self.node | ||
42 | )?; | ||
43 | match &self.matched { | ||
44 | Ok(_) => write!(f, "Node matched")?, | ||
45 | Err(reason) => write!(f, "Node failed to match because: {}", reason.reason)?, | ||
46 | } | ||
47 | Ok(()) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | impl SsrMatches { | ||
52 | /// Returns `self` with any nested matches removed and made into top-level matches. | ||
53 | pub(crate) fn flattened(self) -> SsrMatches { | ||
54 | let mut out = SsrMatches::default(); | ||
55 | self.flatten_into(&mut out); | ||
56 | out | ||
57 | } | ||
58 | |||
59 | fn flatten_into(self, out: &mut SsrMatches) { | ||
60 | for mut m in self.matches { | ||
61 | for p in m.placeholder_values.values_mut() { | ||
62 | std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out); | ||
63 | } | ||
64 | out.matches.push(m); | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | |||
69 | impl Match { | ||
70 | pub(crate) fn matched_text(&self) -> String { | ||
71 | self.matched_node.text().to_string() | ||
72 | } | ||
73 | } | ||
74 | |||
75 | impl<'db> MatchFinder<'db> { | ||
76 | /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you | ||
77 | /// intend to do replacement, use `add_rule` instead. | ||
78 | fn add_search_pattern(&mut self, pattern: SsrPattern) { | ||
79 | self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) | ||
80 | } | ||
81 | |||
82 | /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match | ||
83 | /// them, while recording reasons why they don't match. This API is useful for command | ||
84 | /// line-based debugging where providing a range is difficult. | ||
85 | fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> { | ||
86 | let file = self.sema.parse(file_id); | ||
87 | let mut res = Vec::new(); | ||
88 | let file_text = self.sema.db.file_text(file_id); | ||
89 | let mut remaining_text = file_text.as_str(); | ||
90 | let mut base = 0; | ||
91 | let len = snippet.len() as u32; | ||
92 | while let Some(offset) = remaining_text.find(snippet) { | ||
93 | let start = base + offset as u32; | ||
94 | let end = start + len; | ||
95 | self.output_debug_for_nodes_at_range( | ||
96 | file.syntax(), | ||
97 | TextRange::new(start.into(), end.into()), | ||
98 | &None, | ||
99 | &mut res, | ||
100 | ); | ||
101 | remaining_text = &remaining_text[offset + snippet.len()..]; | ||
102 | base = end; | ||
103 | } | ||
104 | res | ||
105 | } | ||
106 | |||
107 | fn output_debug_for_nodes_at_range( | ||
108 | &self, | ||
109 | node: &SyntaxNode, | ||
110 | range: TextRange, | ||
111 | restrict_range: &Option<FileRange>, | ||
112 | out: &mut Vec<MatchDebugInfo>, | ||
113 | ) { | ||
114 | for node in node.children() { | ||
115 | if !node.text_range().contains_range(range) { | ||
116 | continue; | ||
117 | } | ||
118 | if node.text_range() == range { | ||
119 | for rule in &self.rules { | ||
120 | let pattern = | ||
121 | rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); | ||
122 | out.push(MatchDebugInfo { | ||
123 | matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) | ||
124 | .map_err(|e| MatchFailureReason { | ||
125 | reason: e.reason.unwrap_or_else(|| { | ||
126 | "Match failed, but no reason was given".to_owned() | ||
127 | }), | ||
128 | }), | ||
129 | pattern, | ||
130 | node: node.clone(), | ||
131 | }); | ||
132 | } | ||
133 | } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) { | ||
134 | if let Some(expanded) = self.sema.expand(¯o_call) { | ||
135 | if let Some(tt) = macro_call.token_tree() { | ||
136 | self.output_debug_for_nodes_at_range( | ||
137 | &expanded, | ||
138 | range, | ||
139 | &Some(self.sema.original_range(tt.syntax())), | ||
140 | out, | ||
141 | ); | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | 3 | ||
149 | fn parse_error_text(query: &str) -> String { | 4 | fn parse_error_text(query: &str) -> String { |
150 | format!("{}", query.parse::<SsrRule>().unwrap_err()) | 5 | format!("{}", query.parse::<SsrRule>().unwrap_err()) |
@@ -260,6 +115,19 @@ fn assert_no_match(pattern: &str, code: &str) { | |||
260 | assert_matches(pattern, code, &[]); | 115 | assert_matches(pattern, code, &[]); |
261 | } | 116 | } |
262 | 117 | ||
118 | fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { | ||
119 | let (db, file_id) = single_file(code); | ||
120 | let mut match_finder = MatchFinder::new(&db); | ||
121 | match_finder.add_search_pattern(pattern.parse().unwrap()); | ||
122 | let mut reasons = Vec::new(); | ||
123 | for d in match_finder.debug_where_text_equal(file_id, snippet) { | ||
124 | if let Some(reason) = d.match_failure_reason() { | ||
125 | reasons.push(reason.to_owned()); | ||
126 | } | ||
127 | } | ||
128 | assert_eq!(reasons, vec![expected_reason]); | ||
129 | } | ||
130 | |||
263 | #[test] | 131 | #[test] |
264 | fn ssr_function_to_method() { | 132 | fn ssr_function_to_method() { |
265 | assert_ssr_transform( | 133 | assert_ssr_transform( |
@@ -623,3 +491,30 @@ fn preserves_whitespace_within_macro_expansion() { | |||
623 | fn f() {macro1!(4 - 3 - 1 * 2}"#, | 491 | fn f() {macro1!(4 - 3 - 1 * 2}"#, |
624 | ) | 492 | ) |
625 | } | 493 | } |
494 | |||
495 | #[test] | ||
496 | fn match_failure_reasons() { | ||
497 | let code = r#" | ||
498 | macro_rules! foo { | ||
499 | ($a:expr) => { | ||
500 | 1 + $a + 2 | ||
501 | }; | ||
502 | } | ||
503 | fn f1() { | ||
504 | bar(1, 2); | ||
505 | foo!(5 + 43.to_string() + 5); | ||
506 | } | ||
507 | "#; | ||
508 | assert_match_failure_reason( | ||
509 | "bar($a, 3)", | ||
510 | code, | ||
511 | "bar(1, 2)", | ||
512 | r#"Pattern wanted token '3' (INT_NUMBER), but code had token '2' (INT_NUMBER)"#, | ||
513 | ); | ||
514 | assert_match_failure_reason( | ||
515 | "42.to_string()", | ||
516 | code, | ||
517 | "43.to_string()", | ||
518 | r#"Pattern wanted token '42' (INT_NUMBER), but code had token '43' (INT_NUMBER)"#, | ||
519 | ); | ||
520 | } | ||
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 @@ | |||
5 | 5 | ||
6 | use anyhow::{bail, Result}; | 6 | use anyhow::{bail, Result}; |
7 | use pico_args::Arguments; | 7 | use pico_args::Arguments; |
8 | use ra_ssr::SsrRule; | 8 | use ra_ssr::{SsrPattern, SsrRule}; |
9 | use rust_analyzer::cli::{BenchWhat, Position, Verbosity}; | 9 | use rust_analyzer::cli::{BenchWhat, Position, Verbosity}; |
10 | 10 | ||
11 | use std::{fmt::Write, path::PathBuf}; | 11 | use std::{fmt::Write, path::PathBuf}; |
@@ -50,6 +50,10 @@ pub(crate) enum Command { | |||
50 | Ssr { | 50 | Ssr { |
51 | rules: Vec<SsrRule>, | 51 | rules: Vec<SsrRule>, |
52 | }, | 52 | }, |
53 | StructuredSearch { | ||
54 | debug_snippet: Option<String>, | ||
55 | patterns: Vec<SsrPattern>, | ||
56 | }, | ||
53 | ProcMacro, | 57 | ProcMacro, |
54 | RunServer, | 58 | RunServer, |
55 | Version, | 59 | Version, |
@@ -294,6 +298,7 @@ EXAMPLE: | |||
294 | rust-analyzer ssr '$a.foo($b) ==> bar($a, $b)' | 298 | rust-analyzer ssr '$a.foo($b) ==> bar($a, $b)' |
295 | 299 | ||
296 | FLAGS: | 300 | FLAGS: |
301 | --debug <snippet> Prints debug information for any nodes with source exactly equal to <snippet> | ||
297 | -h, --help Prints help information | 302 | -h, --help Prints help information |
298 | 303 | ||
299 | ARGS: | 304 | ARGS: |
@@ -307,6 +312,34 @@ ARGS: | |||
307 | } | 312 | } |
308 | Command::Ssr { rules } | 313 | Command::Ssr { rules } |
309 | } | 314 | } |
315 | "search" => { | ||
316 | if matches.contains(["-h", "--help"]) { | ||
317 | eprintln!( | ||
318 | "\ | ||
319 | rust-analyzer search | ||
320 | |||
321 | USAGE: | ||
322 | rust-analyzer search [FLAGS] [PATTERN...] | ||
323 | |||
324 | EXAMPLE: | ||
325 | rust-analyzer search '$a.foo($b)' | ||
326 | |||
327 | FLAGS: | ||
328 | --debug <snippet> Prints debug information for any nodes with source exactly equal to <snippet> | ||
329 | -h, --help Prints help information | ||
330 | |||
331 | ARGS: | ||
332 | <PATTERN> A structured search pattern" | ||
333 | ); | ||
334 | return Ok(Err(HelpPrinted)); | ||
335 | } | ||
336 | let debug_snippet = matches.opt_value_from_str("--debug")?; | ||
337 | let mut patterns = Vec::new(); | ||
338 | while let Some(rule) = matches.free_from_str()? { | ||
339 | patterns.push(rule); | ||
340 | } | ||
341 | Command::StructuredSearch { patterns, debug_snippet } | ||
342 | } | ||
310 | _ => { | 343 | _ => { |
311 | print_subcommands(); | 344 | print_subcommands(); |
312 | return Ok(Err(HelpPrinted)); | 345 | return Ok(Err(HelpPrinted)); |
@@ -334,6 +367,7 @@ SUBCOMMANDS: | |||
334 | diagnostics | 367 | diagnostics |
335 | proc-macro | 368 | proc-macro |
336 | parse | 369 | parse |
370 | search | ||
337 | ssr | 371 | ssr |
338 | symbols" | 372 | symbols" |
339 | ) | 373 | ) |
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<()> { | |||
65 | args::Command::Ssr { rules } => { | 65 | args::Command::Ssr { rules } => { |
66 | cli::apply_ssr_rules(rules)?; | 66 | cli::apply_ssr_rules(rules)?; |
67 | } | 67 | } |
68 | args::Command::StructuredSearch { patterns, debug_snippet } => { | ||
69 | cli::search_for_patterns(patterns, debug_snippet)?; | ||
70 | } | ||
68 | args::Command::Version => println!("rust-analyzer {}", env!("REV")), | 71 | args::Command::Version => println!("rust-analyzer {}", env!("REV")), |
69 | } | 72 | } |
70 | Ok(()) | 73 | 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}; | |||
18 | pub use analysis_stats::analysis_stats; | 18 | pub use analysis_stats::analysis_stats; |
19 | pub use diagnostics::diagnostics; | 19 | pub use diagnostics::diagnostics; |
20 | pub use load_cargo::load_cargo; | 20 | pub use load_cargo::load_cargo; |
21 | pub use ssr::apply_ssr_rules; | 21 | pub use ssr::{apply_ssr_rules, search_for_patterns}; |
22 | 22 | ||
23 | #[derive(Clone, Copy)] | 23 | #[derive(Clone, Copy)] |
24 | pub enum Verbosity { | 24 | 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 @@ | |||
2 | 2 | ||
3 | use crate::cli::{load_cargo::load_cargo, Result}; | 3 | use crate::cli::{load_cargo::load_cargo, Result}; |
4 | use ra_ide::SourceFileEdit; | 4 | use ra_ide::SourceFileEdit; |
5 | use ra_ssr::{MatchFinder, SsrRule}; | 5 | use ra_ssr::{MatchFinder, SsrPattern, SsrRule}; |
6 | 6 | ||
7 | pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> { | 7 | pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> { |
8 | use ra_db::SourceDatabaseExt; | 8 | use ra_db::SourceDatabaseExt; |
@@ -31,3 +31,41 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> { | |||
31 | } | 31 | } |
32 | Ok(()) | 32 | Ok(()) |
33 | } | 33 | } |
34 | |||
35 | /// Searches for `patterns`, printing debug information for any nodes whose text exactly matches | ||
36 | /// `debug_snippet`. This is intended for debugging and probably isn't in it's current form useful | ||
37 | /// for much else. | ||
38 | pub fn search_for_patterns(patterns: Vec<SsrPattern>, debug_snippet: Option<String>) -> Result<()> { | ||
39 | use ra_db::SourceDatabaseExt; | ||
40 | use ra_ide_db::symbol_index::SymbolsDatabase; | ||
41 | let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?; | ||
42 | let db = host.raw_database(); | ||
43 | let mut match_finder = MatchFinder::new(db); | ||
44 | for pattern in patterns { | ||
45 | match_finder.add_search_pattern(pattern); | ||
46 | } | ||
47 | for &root in db.local_roots().iter() { | ||
48 | let sr = db.source_root(root); | ||
49 | for file_id in sr.iter() { | ||
50 | if let Some(debug_snippet) = &debug_snippet { | ||
51 | for debug_info in match_finder.debug_where_text_equal(file_id, debug_snippet) { | ||
52 | println!("{:#?}", debug_info); | ||
53 | } | ||
54 | } else { | ||
55 | let matches = match_finder.find_matches_in_file(file_id); | ||
56 | if !matches.matches.is_empty() { | ||
57 | let matches = matches.flattened().matches; | ||
58 | if let Some(path) = vfs.file_path(file_id).as_path() { | ||
59 | println!("{} matches in '{}'", matches.len(), path.to_string_lossy()); | ||
60 | } | ||
61 | // We could possibly at some point do something more useful than just printing | ||
62 | // the matched text. For now though, that's the easiest thing to do. | ||
63 | for m in matches { | ||
64 | println!("{}", m.matched_text()); | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | } | ||
70 | Ok(()) | ||
71 | } | ||