aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_ssr/src/lib.rs
diff options
context:
space:
mode:
authorChetan Khilosiya <[email protected]>2021-02-22 19:14:58 +0000
committerChetan Khilosiya <[email protected]>2021-02-22 19:29:16 +0000
commiteb6cfa7f157690480fca5d55c69dba3fae87ad4f (patch)
treea49a763fee848041fd607f449ad13a0b1040636e /crates/ide_ssr/src/lib.rs
parente4756cb4f6e66097638b9d101589358976be2ba8 (diff)
7526: Renamed create ssr to ide_ssr.
Diffstat (limited to 'crates/ide_ssr/src/lib.rs')
-rw-r--r--crates/ide_ssr/src/lib.rs347
1 files changed, 347 insertions, 0 deletions
diff --git a/crates/ide_ssr/src/lib.rs b/crates/ide_ssr/src/lib.rs
new file mode 100644
index 000000000..a97fc8bca
--- /dev/null
+++ b/crates/ide_ssr/src/lib.rs
@@ -0,0 +1,347 @@
1//! Structural Search Replace
2//!
3//! Allows searching the AST for code that matches one or more patterns and then replacing that code
4//! based on a template.
5
6// Feature: Structural Search and Replace
7//
8// Search and replace with named wildcards that will match any expression, type, path, pattern or item.
9// The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`.
10// A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement.
11// Within a macro call, a placeholder will match up until whatever token follows the placeholder.
12//
13// All paths in both the search pattern and the replacement template must resolve in the context
14// in which this command is invoked. Paths in the search pattern will then match the code if they
15// resolve to the same item, even if they're written differently. For example if we invoke the
16// command in the module `foo` with a pattern of `Bar`, then code in the parent module that refers
17// to `foo::Bar` will match.
18//
19// Paths in the replacement template will be rendered appropriately for the context in which the
20// replacement occurs. For example if our replacement template is `foo::Bar` and we match some
21// code in the `foo` module, we'll insert just `Bar`.
22//
23// Inherent method calls should generally be written in UFCS form. e.g. `foo::Bar::baz($s, $a)` will
24// match `$s.baz($a)`, provided the method call `baz` resolves to the method `foo::Bar::baz`. When a
25// placeholder is the receiver of a method call in the search pattern (e.g. `$s.foo()`), but not in
26// the replacement template (e.g. `bar($s)`), then *, & and &mut will be added as needed to mirror
27// whatever autoderef and autoref was happening implicitly in the matched code.
28//
29// The scope of the search / replace will be restricted to the current selection if any, otherwise
30// it will apply to the whole workspace.
31//
32// Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`.
33//
34// Supported constraints:
35//
36// |===
37// | Constraint | Restricts placeholder
38//
39// | kind(literal) | Is a literal (e.g. `42` or `"forty two"`)
40// | not(a) | Negates the constraint `a`
41// |===
42//
43// Available via the command `rust-analyzer.ssr`.
44//
45// ```rust
46// // Using structural search replace command [foo($a, $b) ==>> ($a).foo($b)]
47//
48// // BEFORE
49// String::from(foo(y + 5, z))
50//
51// // AFTER
52// String::from((y + 5).foo(z))
53// ```
54//
55// |===
56// | Editor | Action Name
57//
58// | VS Code | **Rust Analyzer: Structural Search Replace**
59// |===
60
61mod matching;
62mod nester;
63mod parsing;
64mod replacing;
65mod resolving;
66mod search;
67#[macro_use]
68mod errors;
69#[cfg(test)]
70mod tests;
71
72use crate::errors::bail;
73pub use crate::errors::SsrError;
74pub use crate::matching::Match;
75use crate::matching::MatchFailureReason;
76use hir::Semantics;
77use ide_db::base_db::{FileId, FilePosition, FileRange};
78use resolving::ResolvedRule;
79use rustc_hash::FxHashMap;
80use syntax::{ast, AstNode, SyntaxNode, TextRange};
81use text_edit::TextEdit;
82
83// A structured search replace rule. Create by calling `parse` on a str.
84#[derive(Debug)]
85pub struct SsrRule {
86 /// A structured pattern that we're searching for.
87 pattern: parsing::RawPattern,
88 /// What we'll replace it with.
89 template: parsing::RawPattern,
90 parsed_rules: Vec<parsing::ParsedRule>,
91}
92
93#[derive(Debug)]
94pub struct SsrPattern {
95 raw: parsing::RawPattern,
96 parsed_rules: Vec<parsing::ParsedRule>,
97}
98
99#[derive(Debug, Default)]
100pub struct SsrMatches {
101 pub matches: Vec<Match>,
102}
103
104/// Searches a crate for pattern matches and possibly replaces them with something else.
105pub struct MatchFinder<'db> {
106 /// Our source of information about the user's code.
107 sema: Semantics<'db, ide_db::RootDatabase>,
108 rules: Vec<ResolvedRule>,
109 resolution_scope: resolving::ResolutionScope<'db>,
110 restrict_ranges: Vec<FileRange>,
111}
112
113impl<'db> MatchFinder<'db> {
114 /// Constructs a new instance where names will be looked up as if they appeared at
115 /// `lookup_context`.
116 pub fn in_context(
117 db: &'db ide_db::RootDatabase,
118 lookup_context: FilePosition,
119 mut restrict_ranges: Vec<FileRange>,
120 ) -> MatchFinder<'db> {
121 restrict_ranges.retain(|range| !range.range.is_empty());
122 let sema = Semantics::new(db);
123 let resolution_scope = resolving::ResolutionScope::new(&sema, lookup_context);
124 MatchFinder { sema, rules: Vec::new(), resolution_scope, restrict_ranges }
125 }
126
127 /// Constructs an instance using the start of the first file in `db` as the lookup context.
128 pub fn at_first_file(db: &'db ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
129 use ide_db::base_db::SourceDatabaseExt;
130 use ide_db::symbol_index::SymbolsDatabase;
131 if let Some(first_file_id) = db
132 .local_roots()
133 .iter()
134 .next()
135 .and_then(|root| db.source_root(root.clone()).iter().next())
136 {
137 Ok(MatchFinder::in_context(
138 db,
139 FilePosition { file_id: first_file_id, offset: 0.into() },
140 vec![],
141 ))
142 } else {
143 bail!("No files to search");
144 }
145 }
146
147 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
148 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
149 /// match to it.
150 pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
151 for parsed_rule in rule.parsed_rules {
152 self.rules.push(ResolvedRule::new(
153 parsed_rule,
154 &self.resolution_scope,
155 self.rules.len(),
156 )?);
157 }
158 Ok(())
159 }
160
161 /// Finds matches for all added rules and returns edits for all found matches.
162 pub fn edits(&self) -> FxHashMap<FileId, TextEdit> {
163 use ide_db::base_db::SourceDatabaseExt;
164 let mut matches_by_file = FxHashMap::default();
165 for m in self.matches().matches {
166 matches_by_file
167 .entry(m.range.file_id)
168 .or_insert_with(|| SsrMatches::default())
169 .matches
170 .push(m);
171 }
172 matches_by_file
173 .into_iter()
174 .map(|(file_id, matches)| {
175 (
176 file_id,
177 replacing::matches_to_edit(
178 &matches,
179 &self.sema.db.file_text(file_id),
180 &self.rules,
181 ),
182 )
183 })
184 .collect()
185 }
186
187 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
188 /// intend to do replacement, use `add_rule` instead.
189 pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
190 for parsed_rule in pattern.parsed_rules {
191 self.rules.push(ResolvedRule::new(
192 parsed_rule,
193 &self.resolution_scope,
194 self.rules.len(),
195 )?);
196 }
197 Ok(())
198 }
199
200 /// Returns matches for all added rules.
201 pub fn matches(&self) -> SsrMatches {
202 let mut matches = Vec::new();
203 let mut usage_cache = search::UsageCache::default();
204 for rule in &self.rules {
205 self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
206 }
207 nester::nest_and_remove_collisions(matches, &self.sema)
208 }
209
210 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
211 /// them, while recording reasons why they don't match. This API is useful for command
212 /// line-based debugging where providing a range is difficult.
213 pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> {
214 use ide_db::base_db::SourceDatabaseExt;
215 let file = self.sema.parse(file_id);
216 let mut res = Vec::new();
217 let file_text = self.sema.db.file_text(file_id);
218 let mut remaining_text = file_text.as_str();
219 let mut base = 0;
220 let len = snippet.len() as u32;
221 while let Some(offset) = remaining_text.find(snippet) {
222 let start = base + offset as u32;
223 let end = start + len;
224 self.output_debug_for_nodes_at_range(
225 file.syntax(),
226 FileRange { file_id, range: TextRange::new(start.into(), end.into()) },
227 &None,
228 &mut res,
229 );
230 remaining_text = &remaining_text[offset + snippet.len()..];
231 base = end;
232 }
233 res
234 }
235
236 fn output_debug_for_nodes_at_range(
237 &self,
238 node: &SyntaxNode,
239 range: FileRange,
240 restrict_range: &Option<FileRange>,
241 out: &mut Vec<MatchDebugInfo>,
242 ) {
243 for node in node.children() {
244 let node_range = self.sema.original_range(&node);
245 if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
246 {
247 continue;
248 }
249 if node_range.range == range.range {
250 for rule in &self.rules {
251 // For now we ignore rules that have a different kind than our node, otherwise
252 // we get lots of noise. If at some point we add support for restricting rules
253 // to a particular kind of thing (e.g. only match type references), then we can
254 // relax this. We special-case expressions, since function calls can match
255 // method calls.
256 if rule.pattern.node.kind() != node.kind()
257 && !(ast::Expr::can_cast(rule.pattern.node.kind())
258 && ast::Expr::can_cast(node.kind()))
259 {
260 continue;
261 }
262 out.push(MatchDebugInfo {
263 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
264 .map_err(|e| MatchFailureReason {
265 reason: e.reason.unwrap_or_else(|| {
266 "Match failed, but no reason was given".to_owned()
267 }),
268 }),
269 pattern: rule.pattern.node.clone(),
270 node: node.clone(),
271 });
272 }
273 } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
274 if let Some(expanded) = self.sema.expand(&macro_call) {
275 if let Some(tt) = macro_call.token_tree() {
276 self.output_debug_for_nodes_at_range(
277 &expanded,
278 range,
279 &Some(self.sema.original_range(tt.syntax())),
280 out,
281 );
282 }
283 }
284 }
285 self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
286 }
287 }
288}
289
290pub struct MatchDebugInfo {
291 node: SyntaxNode,
292 /// Our search pattern parsed as an expression or item, etc
293 pattern: SyntaxNode,
294 matched: Result<Match, MatchFailureReason>,
295}
296
297impl std::fmt::Debug for MatchDebugInfo {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 match &self.matched {
300 Ok(_) => writeln!(f, "Node matched")?,
301 Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?,
302 }
303 writeln!(
304 f,
305 "============ AST ===========\n\
306 {:#?}",
307 self.node
308 )?;
309 writeln!(f, "========= PATTERN ==========")?;
310 writeln!(f, "{:#?}", self.pattern)?;
311 writeln!(f, "============================")?;
312 Ok(())
313 }
314}
315
316impl SsrMatches {
317 /// Returns `self` with any nested matches removed and made into top-level matches.
318 pub fn flattened(self) -> SsrMatches {
319 let mut out = SsrMatches::default();
320 self.flatten_into(&mut out);
321 out
322 }
323
324 fn flatten_into(self, out: &mut SsrMatches) {
325 for mut m in self.matches {
326 for p in m.placeholder_values.values_mut() {
327 std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out);
328 }
329 out.matches.push(m);
330 }
331 }
332}
333
334impl Match {
335 pub fn matched_text(&self) -> String {
336 self.matched_node.text().to_string()
337 }
338}
339
340impl std::error::Error for SsrError {}
341
342#[cfg(test)]
343impl MatchDebugInfo {
344 pub(crate) fn match_failure_reason(&self) -> Option<&str> {
345 self.matched.as_ref().err().map(|r| r.reason.as_str())
346 }
347}