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