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