feat: fix engine (Phase 1.6) + git-aware mv + windows-safe path output

- Edit engine: generic span edits (replace/insert/delete) with stale-index
  guard; mv and fix share one atomic apply/rollback/diff/--json pipeline
- jmove mv|check|fix commands; java rules: unused-import, missing-import
  (unique-FQN insert, ambiguous -> candidates for agents), import-order
  (google style; cross-rule byte conflicts prune by severity and converge)
- mv of tracked files goes through git mv (staged rename, --no-git opt-out,
  moved_via/would_move_via in --json, GIT_ERROR code)
- fix java extract_package losing single-segment packages (bare identifier,
  not scoped_identifier) — broke the FQN index for 'package p;' projects
- core::rel_str: project-relative paths rendered with '/' on every platform
  (human/JSON/diff headers/git pathspecs), Path::display() no longer used
  for output; e2e fixtures: java/fix, java/fix_missing, java/order
This commit is contained in:
loki5512344 2026-09-15 16:10:04 +02:00
parent 0216fd6523
commit 9dda5f4562
Signed by: boba
GPG key ID: 253067914055423B
37 changed files with 2521 additions and 234 deletions

View file

@ -15,7 +15,7 @@ use std::ops::Range;
use std::path::{Path, PathBuf};
use crate::core::index::Index;
use crate::core::{JmoveError, JmoveResult, normalize_rel_path};
use crate::core::{Edit, JmoveError, JmoveResult, normalize_rel_path, rel_str};
use crate::parser::SourceLanguage;
/// One in-file replacement of an import specifier. Only the specifier text
@ -32,6 +32,16 @@ pub struct Rewrite {
pub new_text: String,
}
impl From<&Rewrite> for Edit {
fn from(rewrite: &Rewrite) -> Self {
Edit {
span: rewrite.span.clone(),
old_text: rewrite.old_text.clone(),
new_text: rewrite.new_text.clone(),
}
}
}
/// Complete plan for moving `source` to `target`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MovePlan {
@ -60,7 +70,7 @@ pub fn plan_move(index: &Index, source: &Path, target: &Path) -> JmoveResult<Mov
};
let (source, target) = (rel("source path", source)?, rel("target path", target)?);
if !index.files.contains(&source) {
let s = source.display();
let s = rel_str(&source);
return Err(JmoveError::InvalidArgument(format!(
"source '{s}' is not an indexed file"
)));
@ -71,7 +81,7 @@ pub fn plan_move(index: &Index, source: &Path, target: &Path) -> JmoveResult<Mov
));
}
if index.files.contains(&target) {
let t = target.display();
let t = rel_str(&target);
return Err(JmoveError::PlanRejected(format!(
"target '{t}' already exists"
)));
@ -141,7 +151,7 @@ mod tests {
let keys: Vec<(String, usize)> = plan
.rewrites
.iter()
.map(|r| (r.file.display().to_string(), r.span.start))
.map(|r| (crate::core::rel_str(&r.file), r.span.start))
.collect();
assert_eq!(
keys,