feat: global --source-root — index/mv/check/fix one subtree; deterministic Java FQN resolution in monorepos with duplicate packages (guava vs android/guava); move mv_reject to output.rs
This commit is contained in:
parent
7f568fd0c9
commit
d5d34afb00
13 changed files with 278 additions and 64 deletions
|
|
@ -54,6 +54,15 @@ impl Index {
|
|||
/// Scan `root`, parse every supported source file and build the graph.
|
||||
/// Unreadable or unparseable files are skipped, not fatal.
|
||||
pub fn build(root: &Path) -> JmoveResult<Self> {
|
||||
Self::build_scoped(root, None)
|
||||
}
|
||||
|
||||
/// Like [`build`](Self::build), but when `source_root` (project-relative,
|
||||
/// normalized) is `Some`, only files under that subtree are indexed. This
|
||||
/// is the monorepo escape hatch: `guava` vs `android/guava` declare the
|
||||
/// same FQNs, and scoping the index to one self-contained copy makes
|
||||
/// collision-free resolution (and therefore `mv`/`fix` rewrites) exact.
|
||||
pub fn build_scoped(root: &Path, source_root: Option<&Path>) -> JmoveResult<Self> {
|
||||
let root = root.canonicalize()?;
|
||||
let mut index = Self {
|
||||
root,
|
||||
|
|
@ -62,7 +71,7 @@ impl Index {
|
|||
packages: HashMap::new(),
|
||||
java_classes: JavaClassIndex::default(),
|
||||
};
|
||||
index.scan()?;
|
||||
index.scan(source_root)?;
|
||||
// Resolution needs the complete file set (extension/index guessing)
|
||||
// and the full package map, so it runs as a second pass.
|
||||
let java_classes = JavaClassIndex::new(&index.files, &index.packages);
|
||||
|
|
@ -83,11 +92,17 @@ impl Index {
|
|||
}
|
||||
|
||||
// Walk the project and parse each supported source file, staging the
|
||||
// raw records with `target: None` for the resolution pass above.
|
||||
fn scan(&mut self) -> JmoveResult<()> {
|
||||
// Sorted map: deterministic discovery order.
|
||||
// raw records with `target: None` for the resolution pass above. When
|
||||
// `source_root` (project-relative, normalized) is set, only files under
|
||||
// that subtree are indexed — see [`Index::build_scoped`].
|
||||
fn scan(&mut self, source_root: Option<&Path>) -> JmoveResult<()> {
|
||||
// Sorted map: deterministic discovery order. Walking starts at the
|
||||
// scoped subtree when set, so the rest of the monorepo is not even
|
||||
// opened; paths stay root-relative because the prefix removed is
|
||||
// always `self.root`.
|
||||
let mut found: BTreeMap<PathBuf, SourceLanguage> = BTreeMap::new();
|
||||
for entry in WalkBuilder::new(&self.root).require_git(false).build() {
|
||||
let base = source_root.map_or(self.root.clone(), |scope| self.root.join(scope));
|
||||
for entry in WalkBuilder::new(base).require_git(false).build() {
|
||||
// Walker errors (unreadable dirs, etc.) simply skip the entry.
|
||||
let Ok(entry) = entry else { continue };
|
||||
if entry.path_is_symlink() || !entry.file_type().is_some_and(|t| t.is_file()) {
|
||||
|
|
|
|||
|
|
@ -119,3 +119,81 @@ fn importers_of_returns_sorted_reverse_edges() -> JmoveResult<()> {
|
|||
assert!(index.importers_of(Path::new("missing.ts")).is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The guava monorepo shape: `android/guava` and `guava` both declare
|
||||
// `com.example.App`, so an unscoped index resolves every importer to the
|
||||
// same (sorted-first) copy and a move in one tree rewrites the wrong files.
|
||||
fn monorepo() -> JmoveResult<tempfile::TempDir> {
|
||||
let dir = tempfile::TempDir::new()?;
|
||||
let root = dir.path();
|
||||
for tree in ["guava", "android/guava"] {
|
||||
write_file(
|
||||
root,
|
||||
&format!("{tree}/src/com/example/App.java"),
|
||||
"package com.example;\npublic class App {}\n",
|
||||
)?;
|
||||
write_file(
|
||||
root,
|
||||
&format!("{tree}/src/com/example/Use.java"),
|
||||
"package com.example;\nimport com.example.App;\nclass Use { App a; }\n",
|
||||
)?;
|
||||
}
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn resolved_target(index: &Index, importer: &str) -> Option<PathBuf> {
|
||||
index
|
||||
.imports
|
||||
.get(Path::new(importer))?
|
||||
.iter()
|
||||
.find_map(|r| r.target.clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unscoped_index_resolves_the_duplicate_fqn_to_the_sorted_first_tree() -> JmoveResult<()> {
|
||||
let dir = monorepo()?;
|
||||
let index = Index::build(dir.path())?;
|
||||
// Both trees' `App` collide; the class index keeps `android/guava` (sorts first).
|
||||
assert_eq!(
|
||||
resolved_target(&index, "guava/src/com/example/Use.java").as_deref(),
|
||||
Some(Path::new("android/guava/src/com/example/App.java")),
|
||||
"demonstrates the collision the flag exists to fix"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_root_scopes_indexing_so_the_fqn_resolves_within_the_tree() -> JmoveResult<()> {
|
||||
let dir = monorepo()?;
|
||||
let index = Index::build_scoped(dir.path(), Some(Path::new("guava")))?;
|
||||
// Only the guava tree is indexed at all.
|
||||
assert!(
|
||||
index
|
||||
.files
|
||||
.contains(Path::new("guava/src/com/example/App.java"))
|
||||
);
|
||||
assert!(
|
||||
!index
|
||||
.files
|
||||
.contains(Path::new("android/guava/src/com/example/App.java"))
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_target(&index, "guava/src/com/example/Use.java").as_deref(),
|
||||
Some(Path::new("guava/src/com/example/App.java")),
|
||||
"the scoped index resolves to the same tree"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_root_does_not_leak_siblings_sharing_a_name_prefix() -> JmoveResult<()> {
|
||||
// `--source-root guava` must match the directory `guava`, not `guavaX`.
|
||||
let dir = tempfile::TempDir::new()?;
|
||||
let root = dir.path();
|
||||
write_file(root, "guava/src/A.java", "package p;\npublic class A {}\n")?;
|
||||
write_file(root, "guavax/src/B.java", "package p;\npublic class B {}\n")?;
|
||||
let index = Index::build_scoped(root, Some(Path::new("guava")))?;
|
||||
assert!(index.files.contains(Path::new("guava/src/A.java")));
|
||||
assert!(!index.files.contains(Path::new("guavax/src/B.java")));
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue