Move iteration over all files into the SSR crate

The methods `edits_for_file` and `find_matches_in_file` are replaced with just `edits` and `matches`. This simplifies the API a bit, but more importantly it makes it possible in a subsequent commit for SSR to decide to not search all files.
This commit is contained in:
David Lattimore 2020-07-22 16:23:43 +10:00
parent 13f901f636
commit a45682ed96
6 changed files with 92 additions and 89 deletions

View File

@ -1,5 +1,4 @@
use ra_db::SourceDatabaseExt; use ra_ide_db::RootDatabase;
use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase};
use crate::SourceFileEdit; use crate::SourceFileEdit;
use ra_ssr::{MatchFinder, SsrError, SsrRule}; use ra_ssr::{MatchFinder, SsrError, SsrRule};
@ -44,20 +43,11 @@ pub fn parse_search_replace(
parse_only: bool, parse_only: bool,
db: &RootDatabase, db: &RootDatabase,
) -> Result<Vec<SourceFileEdit>, SsrError> { ) -> Result<Vec<SourceFileEdit>, SsrError> {
let mut edits = vec![];
let rule: SsrRule = rule.parse()?; let rule: SsrRule = rule.parse()?;
if parse_only { if parse_only {
return Ok(edits); return Ok(Vec::new());
} }
let mut match_finder = MatchFinder::new(db); let mut match_finder = MatchFinder::new(db);
match_finder.add_rule(rule); match_finder.add_rule(rule);
for &root in db.local_roots().iter() { Ok(match_finder.edits())
let sr = db.source_root(root);
for file_id in sr.iter() {
if let Some(edit) = match_finder.edits_for_file(file_id) {
edits.push(SourceFileEdit { file_id, edit });
}
}
}
Ok(edits)
} }

View File

@ -17,8 +17,9 @@ pub use crate::matching::Match;
use crate::matching::MatchFailureReason; use crate::matching::MatchFailureReason;
use hir::Semantics; use hir::Semantics;
use ra_db::{FileId, FileRange}; use ra_db::{FileId, FileRange};
use ra_ide_db::source_change::SourceFileEdit;
use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
use ra_text_edit::TextEdit; use rustc_hash::FxHashMap;
// A structured search replace rule. Create by calling `parse` on a str. // A structured search replace rule. Create by calling `parse` on a str.
#[derive(Debug)] #[derive(Debug)]
@ -60,32 +61,37 @@ impl<'db> MatchFinder<'db> {
self.add_parsed_rules(rule.parsed_rules); self.add_parsed_rules(rule.parsed_rules);
} }
/// Finds matches for all added rules and returns edits for all found matches.
pub fn edits(&self) -> Vec<SourceFileEdit> {
use ra_db::SourceDatabaseExt;
let mut matches_by_file = FxHashMap::default();
for m in self.matches().matches {
matches_by_file
.entry(m.range.file_id)
.or_insert_with(|| SsrMatches::default())
.matches
.push(m);
}
let mut edits = vec![];
for (file_id, matches) in matches_by_file {
let edit =
replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules);
edits.push(SourceFileEdit { file_id, edit });
}
edits
}
/// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
/// intend to do replacement, use `add_rule` instead. /// intend to do replacement, use `add_rule` instead.
pub fn add_search_pattern(&mut self, pattern: SsrPattern) { pub fn add_search_pattern(&mut self, pattern: SsrPattern) {
self.add_parsed_rules(pattern.parsed_rules); self.add_parsed_rules(pattern.parsed_rules);
} }
pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { /// Returns matches for all added rules.
let matches = self.find_matches_in_file(file_id); pub fn matches(&self) -> SsrMatches {
if matches.matches.is_empty() { let mut matches = Vec::new();
None self.find_all_matches(&mut matches);
} else { SsrMatches { matches }
use ra_db::SourceDatabaseExt;
Some(replacing::matches_to_edit(
&matches,
&self.sema.db.file_text(file_id),
&self.rules,
))
}
}
pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches {
let file = self.sema.parse(file_id);
let code = file.syntax();
let mut matches = SsrMatches::default();
self.slow_scan_node(code, &None, &mut matches.matches);
matches
} }
/// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match

View File

@ -570,13 +570,12 @@ mod tests {
#[test] #[test]
fn parse_match_replace() { fn parse_match_replace() {
let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap(); let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap();
let input = "fn foo() {} fn main() { foo(1+2); }"; let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }";
use ra_db::fixture::WithFixture; let (db, _) = crate::tests::single_file(input);
let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(input);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::new(&db);
match_finder.add_rule(rule); match_finder.add_rule(rule);
let matches = match_finder.find_matches_in_file(file_id); let matches = match_finder.matches();
assert_eq!(matches.matches.len(), 1); assert_eq!(matches.matches.len(), 1);
assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)"); assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)");
assert_eq!(matches.matches[0].placeholder_values.len(), 1); assert_eq!(matches.matches[0].placeholder_values.len(), 1);
@ -589,9 +588,11 @@ mod tests {
"1+2" "1+2"
); );
let edit = crate::replacing::matches_to_edit(&matches, input, &match_finder.rules); let edits = match_finder.edits();
assert_eq!(edits.len(), 1);
let edit = &edits[0];
let mut after = input.to_string(); let mut after = input.to_string();
edit.apply(&mut after); edit.edit.apply(&mut after);
assert_eq!(after, "fn foo() {} fn main() { bar(1+2); }"); assert_eq!(after, "fn foo() {} fn bar() {} fn main() { bar(1+2); }");
} }
} }

View File

@ -5,7 +5,26 @@ use ra_db::FileRange;
use ra_syntax::{ast, AstNode, SyntaxNode}; use ra_syntax::{ast, AstNode, SyntaxNode};
impl<'db> MatchFinder<'db> { impl<'db> MatchFinder<'db> {
pub(crate) fn slow_scan_node( pub(crate) fn find_all_matches(&self, matches_out: &mut Vec<Match>) {
// FIXME: Use resolved paths in the pattern to find places to search instead of always
// scanning every node.
self.slow_scan(matches_out);
}
fn slow_scan(&self, matches_out: &mut Vec<Match>) {
use ra_db::SourceDatabaseExt;
use ra_ide_db::symbol_index::SymbolsDatabase;
for &root in self.sema.db.local_roots().iter() {
let sr = self.sema.db.source_root(root);
for file_id in sr.iter() {
let file = self.sema.parse(file_id);
let code = file.syntax();
self.slow_scan_node(code, &None, matches_out);
}
}
}
fn slow_scan_node(
&self, &self,
code: &SyntaxNode, code: &SyntaxNode,
restrict_range: &Option<FileRange>, restrict_range: &Option<FileRange>,

View File

@ -1,6 +1,8 @@
use crate::{MatchFinder, SsrRule}; use crate::{MatchFinder, SsrRule};
use expect::{expect, Expect}; use expect::{expect, Expect};
use ra_db::{FileId, SourceDatabaseExt}; use ra_db::{salsa::Durability, FileId, SourceDatabaseExt};
use rustc_hash::FxHashSet;
use std::sync::Arc;
use test_utils::mark; use test_utils::mark;
fn parse_error_text(query: &str) -> String { fn parse_error_text(query: &str) -> String {
@ -57,9 +59,15 @@ fn parser_undefined_placeholder_in_replacement() {
); );
} }
fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FileId) { pub(crate) fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FileId) {
use ra_db::fixture::WithFixture; use ra_db::fixture::WithFixture;
ra_ide_db::RootDatabase::with_single_file(code) use ra_ide_db::symbol_index::SymbolsDatabase;
let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(code);
let mut db = db;
let mut local_roots = FxHashSet::default();
local_roots.insert(ra_db::fixture::WORKSPACE);
db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH);
(db, file_id)
} }
fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) { fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) {
@ -73,15 +81,16 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
let rule: SsrRule = rule.parse().unwrap(); let rule: SsrRule = rule.parse().unwrap();
match_finder.add_rule(rule); match_finder.add_rule(rule);
} }
if let Some(edits) = match_finder.edits_for_file(file_id) { let edits = match_finder.edits();
if edits.is_empty() {
panic!("No edits were made");
}
assert_eq!(edits[0].file_id, file_id);
// Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters
// stuff. // stuff.
let mut actual = db.file_text(file_id).to_string(); let mut actual = db.file_text(file_id).to_string();
edits.apply(&mut actual); edits[0].edit.apply(&mut actual);
expected.assert_eq(&actual); expected.assert_eq(&actual);
} else {
panic!("No edits were made");
}
} }
fn print_match_debug_info(match_finder: &MatchFinder, file_id: FileId, snippet: &str) { fn print_match_debug_info(match_finder: &MatchFinder, file_id: FileId, snippet: &str) {
@ -100,13 +109,8 @@ fn assert_matches(pattern: &str, code: &str, expected: &[&str]) {
let (db, file_id) = single_file(code); let (db, file_id) = single_file(code);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::new(&db);
match_finder.add_search_pattern(pattern.parse().unwrap()); match_finder.add_search_pattern(pattern.parse().unwrap());
let matched_strings: Vec<String> = match_finder let matched_strings: Vec<String> =
.find_matches_in_file(file_id) match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect();
.flattened()
.matches
.iter()
.map(|m| m.matched_text())
.collect();
if matched_strings != expected && !expected.is_empty() { if matched_strings != expected && !expected.is_empty() {
print_match_debug_info(&match_finder, file_id, &expected[0]); print_match_debug_info(&match_finder, file_id, &expected[0]);
} }
@ -117,7 +121,7 @@ fn assert_no_match(pattern: &str, code: &str) {
let (db, file_id) = single_file(code); let (db, file_id) = single_file(code);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::new(&db);
match_finder.add_search_pattern(pattern.parse().unwrap()); match_finder.add_search_pattern(pattern.parse().unwrap());
let matches = match_finder.find_matches_in_file(file_id).flattened().matches; let matches = match_finder.matches().flattened().matches;
if !matches.is_empty() { if !matches.is_empty() {
print_match_debug_info(&match_finder, file_id, &matches[0].matched_text()); print_match_debug_info(&match_finder, file_id, &matches[0].matched_text());
panic!("Got {} matches when we expected none: {:#?}", matches.len(), matches); panic!("Got {} matches when we expected none: {:#?}", matches.len(), matches);

View File

@ -1,27 +1,17 @@
//! Applies structured search replace rules from the command line. //! Applies structured search replace rules from the command line.
use crate::cli::{load_cargo::load_cargo, Result}; use crate::cli::{load_cargo::load_cargo, Result};
use ra_ide::SourceFileEdit;
use ra_ssr::{MatchFinder, SsrPattern, SsrRule}; use ra_ssr::{MatchFinder, SsrPattern, SsrRule};
pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> { pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> {
use ra_db::SourceDatabaseExt; use ra_db::SourceDatabaseExt;
use ra_ide_db::symbol_index::SymbolsDatabase;
let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?; let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?;
let db = host.raw_database(); let db = host.raw_database();
let mut match_finder = MatchFinder::new(db); let mut match_finder = MatchFinder::new(db);
for rule in rules { for rule in rules {
match_finder.add_rule(rule); match_finder.add_rule(rule);
} }
let mut edits = Vec::new(); let edits = match_finder.edits();
for &root in db.local_roots().iter() {
let sr = db.source_root(root);
for file_id in sr.iter() {
if let Some(edit) = match_finder.edits_for_file(file_id) {
edits.push(SourceFileEdit { file_id, edit });
}
}
}
for edit in edits { for edit in edits {
if let Some(path) = vfs.file_path(edit.file_id).as_path() { if let Some(path) = vfs.file_path(edit.file_id).as_path() {
let mut contents = db.file_text(edit.file_id).to_string(); let mut contents = db.file_text(edit.file_id).to_string();
@ -38,34 +28,27 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> {
pub fn search_for_patterns(patterns: Vec<SsrPattern>, debug_snippet: Option<String>) -> Result<()> { pub fn search_for_patterns(patterns: Vec<SsrPattern>, debug_snippet: Option<String>) -> Result<()> {
use ra_db::SourceDatabaseExt; use ra_db::SourceDatabaseExt;
use ra_ide_db::symbol_index::SymbolsDatabase; use ra_ide_db::symbol_index::SymbolsDatabase;
let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?; let (host, _vfs) = load_cargo(&std::env::current_dir()?, true, true)?;
let db = host.raw_database(); let db = host.raw_database();
let mut match_finder = MatchFinder::new(db); let mut match_finder = MatchFinder::new(db);
for pattern in patterns { for pattern in patterns {
match_finder.add_search_pattern(pattern); match_finder.add_search_pattern(pattern);
} }
if let Some(debug_snippet) = &debug_snippet {
for &root in db.local_roots().iter() { for &root in db.local_roots().iter() {
let sr = db.source_root(root); let sr = db.source_root(root);
for file_id in sr.iter() { for file_id in sr.iter() {
if let Some(debug_snippet) = &debug_snippet {
for debug_info in match_finder.debug_where_text_equal(file_id, debug_snippet) { for debug_info in match_finder.debug_where_text_equal(file_id, debug_snippet) {
println!("{:#?}", debug_info); println!("{:#?}", debug_info);
} }
} else {
let matches = match_finder.find_matches_in_file(file_id);
if !matches.matches.is_empty() {
let matches = matches.flattened().matches;
if let Some(path) = vfs.file_path(file_id).as_path() {
println!("{} matches in '{}'", matches.len(), path.to_string_lossy());
} }
}
} else {
for m in match_finder.matches().flattened().matches {
// We could possibly at some point do something more useful than just printing // We could possibly at some point do something more useful than just printing
// the matched text. For now though, that's the easiest thing to do. // the matched text. For now though, that's the easiest thing to do.
for m in matches {
println!("{}", m.matched_text()); println!("{}", m.matched_text());
} }
} }
}
}
}
Ok(()) Ok(())
} }