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

@ -14,10 +14,18 @@
//! - Rewrites must touch **only the specifier string**, never the rest of
//! the statement (KISS + no formatter dependency): that is why
//! [`ImportRecord::span`] is a byte range into the original source.
//! - Fix rules (Phase 1.6) implement [`Fix`] and propose [`FixCandidate`]s:
//! byte [`Edit`](crate::core::Edit)s for one file, run through the same
//! dry-run/atomic engine as `mv`. Deterministic by contract — ambiguity
//! is reported as `auto_fixable: false` for an agent to resolve, never
//! guessed at.
use std::ops::Range;
use std::path::Path;
use crate::core::Edit;
use crate::core::index::Index;
pub mod java;
pub mod resolve;
pub mod ts;
@ -111,3 +119,94 @@ pub fn frontend_for(lang: SourceLanguage) -> Box<dyn Language> {
}
}
}
/// Urgency of a [`FixCandidate`] for the reader.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
/// The code does not compile (or resolves) without the fix.
Error,
/// The code works but is dirty: unused or misordered imports.
Warning,
/// Style note only.
Info,
}
impl Severity {
/// Stable lowercase name used in `--json` output.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Info => "info",
}
}
/// Overlap-resolution priority: on a byte conflict the more urgent
/// rule wins and the other candidate is deferred to the next run.
#[must_use]
pub const fn rank(self) -> u8 {
match self {
Self::Error => 0,
Self::Warning => 1,
Self::Info => 2,
}
}
}
/// One proposed fix for a single file: what is wrong and — when
/// `auto_fixable` — the exact edits that repair it.
#[derive(Debug, Clone)]
pub struct FixCandidate {
/// Stable rule id; also the value accepted by `jmove fix --rule`.
pub rule: &'static str,
/// One-line explanation for humans and agents.
pub message: String,
/// How urgent the fix is.
pub severity: Severity,
/// `false` marks an ambiguous finding an agent or human must resolve;
/// the engine never applies such candidates automatically.
pub auto_fixable: bool,
/// Byte range of the offending construct (issue location for reports,
/// even when `edits` is empty).
pub span: Range<usize>,
/// Edits on this file, ascending by span.
pub edits: Vec<Edit>,
/// Fully-qualified options for an ambiguous finding (`auto_fixable:
/// false`); empty for every auto-fixable or non-lookup candidate.
pub candidates: Vec<String>,
}
/// A deterministic single-file fix rule (Phase 1.6).
pub trait Fix: Send + Sync {
/// Stable rule id reported in candidates and accepted by `--rule`.
fn rule(&self) -> &'static str;
/// Candidates for `path` (project-relative) with the given contents.
/// A rule may only return `auto_fixable: true` edits it can prove
/// safe; anything ambiguous goes out as a non-auto candidate.
fn fixes(&self, path: &Path, source: &str, index: &Index) -> Vec<FixCandidate>;
}
/// Default rule set for `lang` (empty for languages without rules yet).
#[must_use]
pub fn fixers_for(lang: SourceLanguage) -> Vec<Box<dyn Fix>> {
match lang {
SourceLanguage::Java => vec![
Box::new(java::rules::unused_imports::JavaUnusedImports::new()),
Box::new(java::rules::missing_imports::JavaMissingImports::new()),
Box::new(java::rules::import_order::JavaImportOrder::new()),
],
SourceLanguage::TypeScript | SourceLanguage::Tsx | SourceLanguage::JavaScript => Vec::new(),
}
}
/// Every rule id `jmove fix` currently knows about.
#[must_use]
pub fn rule_ids() -> &'static [&'static str] {
&[
java::rules::unused_imports::RULE,
java::rules::missing_imports::RULE,
java::rules::import_order::RULE,
]
}