diff options
Diffstat (limited to 'crates/ra_ssr/src/lib.rs')
-rw-r--r-- | crates/ra_ssr/src/lib.rs | 233 |
1 files changed, 123 insertions, 110 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs index cca4576ce..2fb326b45 100644 --- a/crates/ra_ssr/src/lib.rs +++ b/crates/ra_ssr/src/lib.rs | |||
@@ -4,44 +4,41 @@ | |||
4 | //! based on a template. | 4 | //! based on a template. |
5 | 5 | ||
6 | mod matching; | 6 | mod matching; |
7 | mod nester; | ||
7 | mod parsing; | 8 | mod parsing; |
8 | mod replacing; | 9 | mod replacing; |
10 | mod resolving; | ||
11 | mod search; | ||
9 | #[macro_use] | 12 | #[macro_use] |
10 | mod errors; | 13 | mod errors; |
11 | #[cfg(test)] | 14 | #[cfg(test)] |
12 | mod tests; | 15 | mod tests; |
13 | 16 | ||
17 | use crate::errors::bail; | ||
14 | pub use crate::errors::SsrError; | 18 | pub use crate::errors::SsrError; |
15 | pub use crate::matching::Match; | 19 | pub use crate::matching::Match; |
16 | use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason}; | 20 | use crate::matching::MatchFailureReason; |
17 | use hir::Semantics; | 21 | use hir::Semantics; |
18 | use ra_db::{FileId, FileRange}; | 22 | use ra_db::{FileId, FilePosition, FileRange}; |
19 | use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange}; | 23 | use ra_ide_db::source_change::SourceFileEdit; |
20 | use ra_text_edit::TextEdit; | 24 | use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; |
25 | use resolving::ResolvedRule; | ||
21 | use rustc_hash::FxHashMap; | 26 | use rustc_hash::FxHashMap; |
22 | 27 | ||
23 | // A structured search replace rule. Create by calling `parse` on a str. | 28 | // A structured search replace rule. Create by calling `parse` on a str. |
24 | #[derive(Debug)] | 29 | #[derive(Debug)] |
25 | pub struct SsrRule { | 30 | pub struct SsrRule { |
26 | /// A structured pattern that we're searching for. | 31 | /// A structured pattern that we're searching for. |
27 | pattern: SsrPattern, | 32 | pattern: parsing::RawPattern, |
28 | /// What we'll replace it with. | 33 | /// What we'll replace it with. |
29 | template: parsing::SsrTemplate, | 34 | template: parsing::RawPattern, |
35 | parsed_rules: Vec<parsing::ParsedRule>, | ||
30 | } | 36 | } |
31 | 37 | ||
32 | #[derive(Debug)] | 38 | #[derive(Debug)] |
33 | pub struct SsrPattern { | 39 | pub struct SsrPattern { |
34 | raw: parsing::RawSearchPattern, | 40 | raw: parsing::RawPattern, |
35 | /// Placeholders keyed by the stand-in ident that we use in Rust source code. | 41 | parsed_rules: Vec<parsing::ParsedRule>, |
36 | placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>, | ||
37 | // We store our search pattern, parsed as each different kind of thing we can look for. As we | ||
38 | // traverse the AST, we get the appropriate one of these for the type of node we're on. For many | ||
39 | // search patterns, only some of these will be present. | ||
40 | expr: Option<SyntaxNode>, | ||
41 | type_ref: Option<SyntaxNode>, | ||
42 | item: Option<SyntaxNode>, | ||
43 | path: Option<SyntaxNode>, | ||
44 | pattern: Option<SyntaxNode>, | ||
45 | } | 42 | } |
46 | 43 | ||
47 | #[derive(Debug, Default)] | 44 | #[derive(Debug, Default)] |
@@ -53,40 +50,112 @@ pub struct SsrMatches { | |||
53 | pub struct MatchFinder<'db> { | 50 | pub struct MatchFinder<'db> { |
54 | /// Our source of information about the user's code. | 51 | /// Our source of information about the user's code. |
55 | sema: Semantics<'db, ra_ide_db::RootDatabase>, | 52 | sema: Semantics<'db, ra_ide_db::RootDatabase>, |
56 | rules: Vec<SsrRule>, | 53 | rules: Vec<ResolvedRule>, |
54 | scope: hir::SemanticsScope<'db>, | ||
55 | hygiene: hir::Hygiene, | ||
57 | } | 56 | } |
58 | 57 | ||
59 | impl<'db> MatchFinder<'db> { | 58 | impl<'db> MatchFinder<'db> { |
60 | pub fn new(db: &'db ra_ide_db::RootDatabase) -> MatchFinder<'db> { | 59 | /// Constructs a new instance where names will be looked up as if they appeared at |
61 | MatchFinder { sema: Semantics::new(db), rules: Vec::new() } | 60 | /// `lookup_context`. |
61 | pub fn in_context( | ||
62 | db: &'db ra_ide_db::RootDatabase, | ||
63 | lookup_context: FilePosition, | ||
64 | ) -> MatchFinder<'db> { | ||
65 | let sema = Semantics::new(db); | ||
66 | let file = sema.parse(lookup_context.file_id); | ||
67 | // Find a node at the requested position, falling back to the whole file. | ||
68 | let node = file | ||
69 | .syntax() | ||
70 | .token_at_offset(lookup_context.offset) | ||
71 | .left_biased() | ||
72 | .map(|token| token.parent()) | ||
73 | .unwrap_or_else(|| file.syntax().clone()); | ||
74 | let scope = sema.scope(&node); | ||
75 | MatchFinder { | ||
76 | sema: Semantics::new(db), | ||
77 | rules: Vec::new(), | ||
78 | scope, | ||
79 | hygiene: hir::Hygiene::new(db, lookup_context.file_id.into()), | ||
80 | } | ||
62 | } | 81 | } |
63 | 82 | ||
64 | pub fn add_rule(&mut self, rule: SsrRule) { | 83 | /// Constructs an instance using the start of the first file in `db` as the lookup context. |
65 | self.rules.push(rule); | 84 | pub fn at_first_file(db: &'db ra_ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> { |
85 | use ra_db::SourceDatabaseExt; | ||
86 | use ra_ide_db::symbol_index::SymbolsDatabase; | ||
87 | if let Some(first_file_id) = db | ||
88 | .local_roots() | ||
89 | .iter() | ||
90 | .next() | ||
91 | .and_then(|root| db.source_root(root.clone()).iter().next()) | ||
92 | { | ||
93 | Ok(MatchFinder::in_context( | ||
94 | db, | ||
95 | FilePosition { file_id: first_file_id, offset: 0.into() }, | ||
96 | )) | ||
97 | } else { | ||
98 | bail!("No files to search"); | ||
99 | } | ||
66 | } | 100 | } |
67 | 101 | ||
68 | /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you | 102 | /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take |
69 | /// intend to do replacement, use `add_rule` instead. | 103 | /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to |
70 | pub fn add_search_pattern(&mut self, pattern: SsrPattern) { | 104 | /// match to it. |
71 | self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) | 105 | pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> { |
106 | for parsed_rule in rule.parsed_rules { | ||
107 | self.rules.push(ResolvedRule::new( | ||
108 | parsed_rule, | ||
109 | &self.scope, | ||
110 | &self.hygiene, | ||
111 | self.rules.len(), | ||
112 | )?); | ||
113 | } | ||
114 | Ok(()) | ||
72 | } | 115 | } |
73 | 116 | ||
74 | pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { | 117 | /// Finds matches for all added rules and returns edits for all found matches. |
75 | let matches = self.find_matches_in_file(file_id); | 118 | pub fn edits(&self) -> Vec<SourceFileEdit> { |
76 | if matches.matches.is_empty() { | 119 | use ra_db::SourceDatabaseExt; |
77 | None | 120 | let mut matches_by_file = FxHashMap::default(); |
78 | } else { | 121 | for m in self.matches().matches { |
79 | use ra_db::SourceDatabaseExt; | 122 | matches_by_file |
80 | Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) | 123 | .entry(m.range.file_id) |
124 | .or_insert_with(|| SsrMatches::default()) | ||
125 | .matches | ||
126 | .push(m); | ||
127 | } | ||
128 | let mut edits = vec![]; | ||
129 | for (file_id, matches) in matches_by_file { | ||
130 | let edit = | ||
131 | replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules); | ||
132 | edits.push(SourceFileEdit { file_id, edit }); | ||
81 | } | 133 | } |
134 | edits | ||
82 | } | 135 | } |
83 | 136 | ||
84 | pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { | 137 | /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you |
85 | let file = self.sema.parse(file_id); | 138 | /// intend to do replacement, use `add_rule` instead. |
86 | let code = file.syntax(); | 139 | pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> { |
87 | let mut matches = SsrMatches::default(); | 140 | for parsed_rule in pattern.parsed_rules { |
88 | self.find_matches(code, &None, &mut matches); | 141 | self.rules.push(ResolvedRule::new( |
89 | matches | 142 | parsed_rule, |
143 | &self.scope, | ||
144 | &self.hygiene, | ||
145 | self.rules.len(), | ||
146 | )?); | ||
147 | } | ||
148 | Ok(()) | ||
149 | } | ||
150 | |||
151 | /// Returns matches for all added rules. | ||
152 | pub fn matches(&self) -> SsrMatches { | ||
153 | let mut matches = Vec::new(); | ||
154 | let mut usage_cache = search::UsageCache::default(); | ||
155 | for rule in &self.rules { | ||
156 | self.find_matches_for_rule(rule, &mut usage_cache, &mut matches); | ||
157 | } | ||
158 | nester::nest_and_remove_collisions(matches, &self.sema) | ||
90 | } | 159 | } |
91 | 160 | ||
92 | /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match | 161 | /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match |
@@ -115,53 +184,6 @@ impl<'db> MatchFinder<'db> { | |||
115 | res | 184 | res |
116 | } | 185 | } |
117 | 186 | ||
118 | fn find_matches( | ||
119 | &self, | ||
120 | code: &SyntaxNode, | ||
121 | restrict_range: &Option<FileRange>, | ||
122 | matches_out: &mut SsrMatches, | ||
123 | ) { | ||
124 | for rule in &self.rules { | ||
125 | if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) { | ||
126 | // Continue searching in each of our placeholders. | ||
127 | for placeholder_value in m.placeholder_values.values_mut() { | ||
128 | if let Some(placeholder_node) = &placeholder_value.node { | ||
129 | // Don't search our placeholder if it's the entire matched node, otherwise we'd | ||
130 | // find the same match over and over until we got a stack overflow. | ||
131 | if placeholder_node != code { | ||
132 | self.find_matches( | ||
133 | placeholder_node, | ||
134 | restrict_range, | ||
135 | &mut placeholder_value.inner_matches, | ||
136 | ); | ||
137 | } | ||
138 | } | ||
139 | } | ||
140 | matches_out.matches.push(m); | ||
141 | return; | ||
142 | } | ||
143 | } | ||
144 | // If we've got a macro call, we already tried matching it pre-expansion, which is the only | ||
145 | // way to match the whole macro, now try expanding it and matching the expansion. | ||
146 | if let Some(macro_call) = ast::MacroCall::cast(code.clone()) { | ||
147 | if let Some(expanded) = self.sema.expand(¯o_call) { | ||
148 | if let Some(tt) = macro_call.token_tree() { | ||
149 | // When matching within a macro expansion, we only want to allow matches of | ||
150 | // nodes that originated entirely from within the token tree of the macro call. | ||
151 | // i.e. we don't want to match something that came from the macro itself. | ||
152 | self.find_matches( | ||
153 | &expanded, | ||
154 | &Some(self.sema.original_range(tt.syntax())), | ||
155 | matches_out, | ||
156 | ); | ||
157 | } | ||
158 | } | ||
159 | } | ||
160 | for child in code.children() { | ||
161 | self.find_matches(&child, restrict_range, matches_out); | ||
162 | } | ||
163 | } | ||
164 | |||
165 | fn output_debug_for_nodes_at_range( | 187 | fn output_debug_for_nodes_at_range( |
166 | &self, | 188 | &self, |
167 | node: &SyntaxNode, | 189 | node: &SyntaxNode, |
@@ -177,8 +199,17 @@ impl<'db> MatchFinder<'db> { | |||
177 | } | 199 | } |
178 | if node_range.range == range.range { | 200 | if node_range.range == range.range { |
179 | for rule in &self.rules { | 201 | for rule in &self.rules { |
180 | let pattern = | 202 | // For now we ignore rules that have a different kind than our node, otherwise |
181 | rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); | 203 | // we get lots of noise. If at some point we add support for restricting rules |
204 | // to a particular kind of thing (e.g. only match type references), then we can | ||
205 | // relax this. We special-case expressions, since function calls can match | ||
206 | // method calls. | ||
207 | if rule.pattern.node.kind() != node.kind() | ||
208 | && !(ast::Expr::can_cast(rule.pattern.node.kind()) | ||
209 | && ast::Expr::can_cast(node.kind())) | ||
210 | { | ||
211 | continue; | ||
212 | } | ||
182 | out.push(MatchDebugInfo { | 213 | out.push(MatchDebugInfo { |
183 | matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) | 214 | matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) |
184 | .map_err(|e| MatchFailureReason { | 215 | .map_err(|e| MatchFailureReason { |
@@ -186,7 +217,7 @@ impl<'db> MatchFinder<'db> { | |||
186 | "Match failed, but no reason was given".to_owned() | 217 | "Match failed, but no reason was given".to_owned() |
187 | }), | 218 | }), |
188 | }), | 219 | }), |
189 | pattern, | 220 | pattern: rule.pattern.node.clone(), |
190 | node: node.clone(), | 221 | node: node.clone(), |
191 | }); | 222 | }); |
192 | } | 223 | } |
@@ -209,9 +240,8 @@ impl<'db> MatchFinder<'db> { | |||
209 | 240 | ||
210 | pub struct MatchDebugInfo { | 241 | pub struct MatchDebugInfo { |
211 | node: SyntaxNode, | 242 | node: SyntaxNode, |
212 | /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, | 243 | /// Our search pattern parsed as an expression or item, etc |
213 | /// etc. Will be absent if the pattern can't be parsed as that kind. | 244 | pattern: SyntaxNode, |
214 | pattern: Result<SyntaxNode, MatchFailureReason>, | ||
215 | matched: Result<Match, MatchFailureReason>, | 245 | matched: Result<Match, MatchFailureReason>, |
216 | } | 246 | } |
217 | 247 | ||
@@ -228,29 +258,12 @@ impl std::fmt::Debug for MatchDebugInfo { | |||
228 | self.node | 258 | self.node |
229 | )?; | 259 | )?; |
230 | writeln!(f, "========= PATTERN ==========")?; | 260 | writeln!(f, "========= PATTERN ==========")?; |
231 | match &self.pattern { | 261 | writeln!(f, "{:#?}", self.pattern)?; |
232 | Ok(pattern) => { | ||
233 | writeln!(f, "{:#?}", pattern)?; | ||
234 | } | ||
235 | Err(err) => { | ||
236 | writeln!(f, "{}", err.reason)?; | ||
237 | } | ||
238 | } | ||
239 | writeln!(f, "============================")?; | 262 | writeln!(f, "============================")?; |
240 | Ok(()) | 263 | Ok(()) |
241 | } | 264 | } |
242 | } | 265 | } |
243 | 266 | ||
244 | impl SsrPattern { | ||
245 | fn tree_for_kind_with_reason( | ||
246 | &self, | ||
247 | kind: SyntaxKind, | ||
248 | ) -> Result<&SyntaxNode, MatchFailureReason> { | ||
249 | record_match_fails_reasons_scope(true, || self.tree_for_kind(kind)) | ||
250 | .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() }) | ||
251 | } | ||
252 | } | ||
253 | |||
254 | impl SsrMatches { | 267 | impl SsrMatches { |
255 | /// Returns `self` with any nested matches removed and made into top-level matches. | 268 | /// Returns `self` with any nested matches removed and made into top-level matches. |
256 | pub fn flattened(self) -> SsrMatches { | 269 | pub fn flattened(self) -> SsrMatches { |