aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ssr/src/lib.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2020-08-13 15:45:10 +0100
committerAleksey Kladov <[email protected]>2020-08-13 16:02:44 +0100
commitae3abd6e575940eb1221acf26c09e96352f052fa (patch)
treeb9c7e76342b631709ecc7cea807dd82a43539312 /crates/ra_ssr/src/lib.rs
parentbb5c189b7dae1ea63ccd5d7a0c2e097d7c676f77 (diff)
Rename ra_ssr -> ssr
Diffstat (limited to 'crates/ra_ssr/src/lib.rs')
-rw-r--r--crates/ra_ssr/src/lib.rs286
1 files changed, 0 insertions, 286 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
deleted file mode 100644
index b4e35107e..000000000
--- a/crates/ra_ssr/src/lib.rs
+++ /dev/null
@@ -1,286 +0,0 @@
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
6mod matching;
7mod nester;
8mod parsing;
9mod replacing;
10mod resolving;
11mod search;
12#[macro_use]
13mod errors;
14#[cfg(test)]
15mod tests;
16
17use crate::errors::bail;
18pub use crate::errors::SsrError;
19pub use crate::matching::Match;
20use crate::matching::MatchFailureReason;
21use base_db::{FileId, FilePosition, FileRange};
22use hir::Semantics;
23use ide_db::source_change::SourceFileEdit;
24use resolving::ResolvedRule;
25use rustc_hash::FxHashMap;
26use syntax::{ast, AstNode, SyntaxNode, TextRange};
27
28// A structured search replace rule. Create by calling `parse` on a str.
29#[derive(Debug)]
30pub struct SsrRule {
31 /// A structured pattern that we're searching for.
32 pattern: parsing::RawPattern,
33 /// What we'll replace it with.
34 template: parsing::RawPattern,
35 parsed_rules: Vec<parsing::ParsedRule>,
36}
37
38#[derive(Debug)]
39pub struct SsrPattern {
40 raw: parsing::RawPattern,
41 parsed_rules: Vec<parsing::ParsedRule>,
42}
43
44#[derive(Debug, Default)]
45pub struct SsrMatches {
46 pub matches: Vec<Match>,
47}
48
49/// Searches a crate for pattern matches and possibly replaces them with something else.
50pub struct MatchFinder<'db> {
51 /// Our source of information about the user's code.
52 sema: Semantics<'db, ide_db::RootDatabase>,
53 rules: Vec<ResolvedRule>,
54 resolution_scope: resolving::ResolutionScope<'db>,
55 restrict_ranges: Vec<FileRange>,
56}
57
58impl<'db> MatchFinder<'db> {
59 /// Constructs a new instance where names will be looked up as if they appeared at
60 /// `lookup_context`.
61 pub fn in_context(
62 db: &'db 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 { sema, rules: Vec::new(), resolution_scope, restrict_ranges }
70 }
71
72 /// Constructs an instance using the start of the first file in `db` as the lookup context.
73 pub fn at_first_file(db: &'db ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
74 use base_db::SourceDatabaseExt;
75 use ide_db::symbol_index::SymbolsDatabase;
76 if let Some(first_file_id) = db
77 .local_roots()
78 .iter()
79 .next()
80 .and_then(|root| db.source_root(root.clone()).iter().next())
81 {
82 Ok(MatchFinder::in_context(
83 db,
84 FilePosition { file_id: first_file_id, offset: 0.into() },
85 vec![],
86 ))
87 } else {
88 bail!("No files to search");
89 }
90 }
91
92 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
93 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
94 /// match to it.
95 pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
96 for parsed_rule in rule.parsed_rules {
97 self.rules.push(ResolvedRule::new(
98 parsed_rule,
99 &self.resolution_scope,
100 self.rules.len(),
101 )?);
102 }
103 Ok(())
104 }
105
106 /// Finds matches for all added rules and returns edits for all found matches.
107 pub fn edits(&self) -> Vec<SourceFileEdit> {
108 use base_db::SourceDatabaseExt;
109 let mut matches_by_file = FxHashMap::default();
110 for m in self.matches().matches {
111 matches_by_file
112 .entry(m.range.file_id)
113 .or_insert_with(|| SsrMatches::default())
114 .matches
115 .push(m);
116 }
117 let mut edits = vec![];
118 for (file_id, matches) in matches_by_file {
119 let edit =
120 replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules);
121 edits.push(SourceFileEdit { file_id, edit });
122 }
123 edits
124 }
125
126 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
127 /// intend to do replacement, use `add_rule` instead.
128 pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
129 for parsed_rule in pattern.parsed_rules {
130 self.rules.push(ResolvedRule::new(
131 parsed_rule,
132 &self.resolution_scope,
133 self.rules.len(),
134 )?);
135 }
136 Ok(())
137 }
138
139 /// Returns matches for all added rules.
140 pub fn matches(&self) -> SsrMatches {
141 let mut matches = Vec::new();
142 let mut usage_cache = search::UsageCache::default();
143 for rule in &self.rules {
144 self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
145 }
146 nester::nest_and_remove_collisions(matches, &self.sema)
147 }
148
149 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
150 /// them, while recording reasons why they don't match. This API is useful for command
151 /// line-based debugging where providing a range is difficult.
152 pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> {
153 use base_db::SourceDatabaseExt;
154 let file = self.sema.parse(file_id);
155 let mut res = Vec::new();
156 let file_text = self.sema.db.file_text(file_id);
157 let mut remaining_text = file_text.as_str();
158 let mut base = 0;
159 let len = snippet.len() as u32;
160 while let Some(offset) = remaining_text.find(snippet) {
161 let start = base + offset as u32;
162 let end = start + len;
163 self.output_debug_for_nodes_at_range(
164 file.syntax(),
165 FileRange { file_id, range: TextRange::new(start.into(), end.into()) },
166 &None,
167 &mut res,
168 );
169 remaining_text = &remaining_text[offset + snippet.len()..];
170 base = end;
171 }
172 res
173 }
174
175 fn output_debug_for_nodes_at_range(
176 &self,
177 node: &SyntaxNode,
178 range: FileRange,
179 restrict_range: &Option<FileRange>,
180 out: &mut Vec<MatchDebugInfo>,
181 ) {
182 for node in node.children() {
183 let node_range = self.sema.original_range(&node);
184 if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
185 {
186 continue;
187 }
188 if node_range.range == range.range {
189 for rule in &self.rules {
190 // For now we ignore rules that have a different kind than our node, otherwise
191 // we get lots of noise. If at some point we add support for restricting rules
192 // to a particular kind of thing (e.g. only match type references), then we can
193 // relax this. We special-case expressions, since function calls can match
194 // method calls.
195 if rule.pattern.node.kind() != node.kind()
196 && !(ast::Expr::can_cast(rule.pattern.node.kind())
197 && ast::Expr::can_cast(node.kind()))
198 {
199 continue;
200 }
201 out.push(MatchDebugInfo {
202 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
203 .map_err(|e| MatchFailureReason {
204 reason: e.reason.unwrap_or_else(|| {
205 "Match failed, but no reason was given".to_owned()
206 }),
207 }),
208 pattern: rule.pattern.node.clone(),
209 node: node.clone(),
210 });
211 }
212 } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
213 if let Some(expanded) = self.sema.expand(&macro_call) {
214 if let Some(tt) = macro_call.token_tree() {
215 self.output_debug_for_nodes_at_range(
216 &expanded,
217 range,
218 &Some(self.sema.original_range(tt.syntax())),
219 out,
220 );
221 }
222 }
223 }
224 self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
225 }
226 }
227}
228
229pub struct MatchDebugInfo {
230 node: SyntaxNode,
231 /// Our search pattern parsed as an expression or item, etc
232 pattern: SyntaxNode,
233 matched: Result<Match, MatchFailureReason>,
234}
235
236impl std::fmt::Debug for MatchDebugInfo {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 match &self.matched {
239 Ok(_) => writeln!(f, "Node matched")?,
240 Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?,
241 }
242 writeln!(
243 f,
244 "============ AST ===========\n\
245 {:#?}",
246 self.node
247 )?;
248 writeln!(f, "========= PATTERN ==========")?;
249 writeln!(f, "{:#?}", self.pattern)?;
250 writeln!(f, "============================")?;
251 Ok(())
252 }
253}
254
255impl SsrMatches {
256 /// Returns `self` with any nested matches removed and made into top-level matches.
257 pub fn flattened(self) -> SsrMatches {
258 let mut out = SsrMatches::default();
259 self.flatten_into(&mut out);
260 out
261 }
262
263 fn flatten_into(self, out: &mut SsrMatches) {
264 for mut m in self.matches {
265 for p in m.placeholder_values.values_mut() {
266 std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out);
267 }
268 out.matches.push(m);
269 }
270 }
271}
272
273impl Match {
274 pub fn matched_text(&self) -> String {
275 self.matched_node.text().to_string()
276 }
277}
278
279impl std::error::Error for SsrError {}
280
281#[cfg(test)]
282impl MatchDebugInfo {
283 pub(crate) fn match_failure_reason(&self) -> Option<&str> {
284 self.matched.as_ref().err().map(|r| r.reason.as_str())
285 }
286}