diff --git a/README.md b/README.md index 21b9484..113f034 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,22 @@ jmove's answer: ## Install ```sh +# From a clone of this repo (installs `jmove` into ~/.cargo/bin, on PATH): +cargo install --path . --locked + +# Latest published version (once jmove is on crates.io — not yet published): cargo install jmove ``` +Use as a library (the CLI and the engine are separate targets; `jmove::core` +holds indexing/planning/apply, `jmove::parser` the language frontends): + +```toml +[dependencies] +jmove = { git = "https://github.com/loki5512344/jmove" } +# or, for a local checkout: jmove = { path = "../jmove" } +``` + ## Usage ```sh @@ -40,12 +53,20 @@ jmove mv src/utils/parser.ts src/core/parser.ts --dry-run # Apply it jmove mv src/utils/parser.ts src/core/parser.ts +# Inside a git repo a tracked file moves via `git mv` (staged, history kept); +# --no-git forces a plain rename +jmove mv src/foo.ts src/bar/foo.ts --no-git + # Java: jmove updates `package`, all `import`s and moves the file jmove mv src/com/example/utils/Parser.java src/com/example/core/Parser.java # Find broken imports (exit code 2 if any) jmove check +# Auto-repair import problems (unused + missing imports today) — same dry-run/atomic engine +jmove fix --dry-run +jmove fix + # AI-agent workflow jmove mv src/foo.ts src/bar/foo.ts --dry-run --json jmove mv src/foo.ts src/bar/foo.ts --json @@ -58,10 +79,10 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for more, and ## Roadmap TypeScript/JavaScript and Java (the open niche) are in — real-world tested -on `google/guava`. Next: a `fix` command reusing the same dry-run/atomic -engine to auto-repair small breakages (unused/missing/misordered imports), -then Python, Go. `split` (automatic file decomposition) is planned — no -tool does it. Full plan: [docs/PLAN.md](docs/PLAN.md). +on `google/guava`. `fix` auto-repairs small breakages on the same +dry-run/atomic engine (unused, missing and misordered Java imports; +TS rules next), then Python, Go. `split` (automatic file decomposition) is +planned — no tool does it. Full plan: [docs/PLAN.md](docs/PLAN.md). ## License diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index d330d8f..656288d 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -3,9 +3,11 @@ Concrete examples for both audiences: humans at a terminal and AI agents consuming `--json`. All outputs below are captured from the real binary. -Flags (see `jmove --help`): `mv [--dry-run] [--json]`, -`check [--json]`, and the global `--root `. Paths may be relative to -the root or absolute inside it; `.gitignore`d files are never indexed. +Flags (see `jmove --help`): `mv [--dry-run] [--json] +[--no-git]`, `check [--json]`, and the global `--root `. Paths may be +relative to the root or absolute inside it; `.gitignore`d files are never +indexed. Inside a git repo, `mv` of a tracked file uses `git mv` (the +rename is staged); `--no-git` forces a plain filesystem rename. Exit codes: `0` ok · `1` operation error · `2` `check` found broken imports. ## Human usage @@ -103,6 +105,50 @@ fourth rewrite. Targets outside the source root, non-`.java` targets and cross-directory moves of default-package classes are rejected with `PLAN_REJECTED` (exit 1) and change nothing. +### Fix: auto-repair unused imports + +`fix` runs deterministic rules and applies them through the same +dry-run/atomic engine. Preview first, then apply: + +```console +$ cat src/main/java/com/example/app/App.java +package com.example.app; + +import com.example.Text; +import com.example.unused.Ghost; // never referenced + +... +$ jmove fix --dry-run +--- src/main/java/com/example/app/App.java ++++ src/main/java/com/example/app/App.java +@@ -1,6 +1,5 @@ + package com.example.app; + + import com.example.Text; +-import com.example.unused.Ghost; +... +$ jmove fix +fixed 1 issue in 1 file +``` + +Only provably-dead imports are removed: the whole statement line (with its +newline) disappears and every other line stays byte-identical. A name that +also appears in a comment, string literal or a sibling static import is +kept, so the rule can only under-report, never delete live code. Scope to +one rule with `--rule java/unused-import` (`java/import-order` sorts the block +google-style); an unknown id exits `1` with `INVALID_ARGUMENT` and lists the rules. + +### Fix: add a missing import for a bare type reference + +After a Java `mv` changes a file's package, references to former +same-package siblings stop resolving. `java/missing-import` inserts the +import for a unique FQN in the class index; ambiguous ones are reported. + +```console +$ jmove fix --rule java/missing-import +fixed 1 issue in 1 file # inserted: import com.example.util.Maths; +``` + ## AI-agent usage (`--json`) Every `--json` response is a flat envelope: `status` (`"ok"` | `"dry_run"` @@ -122,6 +168,7 @@ $ jmove mv lib/sum.ts utils/sum.ts --dry-run --json "affected_files": [ "app.ts" ], + "would_move_via": "fs", "diff": "--- app.ts\n+++ app.ts\n@@ -1,4 +1,4 @@\n-import { sum } from \"./lib/sum\";\n+import { sum } from \"./utils/sum\";\n..." } ``` @@ -151,12 +198,15 @@ $ jmove mv lib/sum.ts utils/sum.ts --json } ], "moved": 1, - "updated_imports": 1 + "updated_imports": 1, + "moved_via": "fs" } ``` `changed_files[].changes[]` lists every rewritten specifier with its -1-based line; `moved` and `updated_imports` are the counters. +1-based line; `moved` and `updated_imports` are the counters, +`moved_via` tells whether the rename went through git (`"git"`, staged) +or the plain filesystem (`"fs"`). ### 3. Verify diff --git a/docs/SKILL.md b/docs/SKILL.md index a3916e7..1b05d81 100644 --- a/docs/SKILL.md +++ b/docs/SKILL.md @@ -18,13 +18,21 @@ kept in sync); Python/Go on the roadmap. Single binary, no LSP needed. ### mv — move a file and rewrite its importers ``` -jmove mv [--root DIR] [--dry-run] [--json] +jmove mv [--root DIR] [--dry-run] [--json] [--no-git] ``` Always run `--dry-run` first and confirm the change set looks right. Moving onto an existing path fails with `TARGET_EXISTS` — choose another target (Phase 1 has no overwrite mode). +Git integration: inside a git repository, a tracked file is renamed with +`git mv` so the rename is staged (history-preserving `git log --follow` / +`git diff -M` work). Untracked files, non-repositories and `--no-git` +fall back to a plain filesystem rename. The import rewrites land in the +working tree unstaged either way — stage or commit them yourself. +`--json` reports the choice as `moved_via` (`"git"`/`"fs"`) and, on a +dry-run, `would_move_via`. + ### check — find broken imports ``` @@ -33,6 +41,32 @@ jmove check [--root DIR] [--json] Run after any move (or any edit) to validate project consistency. +### fix — auto-repair import problems + +``` +jmove fix [--root DIR] [--rule ID] [--dry-run] [--json] +``` + +Runs the deterministic rules over the whole project and applies the +repairs through the same atomic engine as `mv` (dry-run diff, rollback, +exit codes). Current Java rules: `java/unused-import` (deletes single-type +imports whose name is provably unreferenced), `java/missing-import` +(inserts the import of a project class used by simple name — unique FQN +candidate required) and `java/import-order` (Google style: statics first, +then single-type, ASCII-sorted, duplicates dropped). Unknown `--rule` +fails with `INVALID_ARGUMENT` and lists the known ids. A candidate the +engine cannot prove safe is reported with `"applied": false` and a +`candidates` array of FQN options — resolve it yourself (pick one, add +the import) and re-run; `fix` never guesses. When two rules want the same +bytes (order rewrite vs an unused deletion), the urgent rule applies and +the other is deferred (`"applied": false`, "skipped") — re-run `fix` +until it reports "nothing to change" (converges in 2–3 runs). + +Recommended: `fix --dry-run --json`, inspect `files[].fixes[]` +(`rule`, `line`, `message`, `applied`, optional `candidates`), then apply +without `--dry-run`. For Java projects the practical loop is: `mv` → +`fix` → `fix` again if anything was "skipped" → `check`. + ## Java specifics - Moving a `.java` file rewrites three coordinated edits: its own @@ -52,7 +86,9 @@ Run after any move (or any edit) to validate project consistency. 1. `jmove mv --dry-run --json` — preview. 2. Inspect `affected_files`; if unexpected, abort and ask the user. 3. `jmove mv --json` — apply (atomic, rolls back on error). -4. `jmove check --json` — verify nothing is broken. +4. Java moves: `jmove fix --json` — heal bare same-package references + left behind by the move (they break `javac` otherwise). +5. `jmove check --json` — verify nothing is broken. ## Output format (--json) diff --git a/todo.md b/todo.md index 99913ae..be0bb39 100644 --- a/todo.md +++ b/todo.md @@ -45,14 +45,17 @@ dry-run/rollback/error-codes не переживают недетерминированный движок). AI оставляем СНАРУЖИ: при неоднозначности jmove отдаёт `candidates` в --json, агент (LLM) выбирает и повторяет команду — в рамках нашего agent-first UX. -- [ ] Edit engine: расширить `rewrite_bytes` с замены span до `Edit{span,new}` - (пустой span = вставка; замена на "" + поглощение `\n` = удаление строки); - apply/rollback/diff/json трогаются минимально (они уже generic над правками) -- [ ] `Fix` trait рядом с `Language`: `fixes(source, ctx) -> Vec` +- [x] Edit engine: `core::Edit{span,old_text,new_text}` — replace/insert/delete в одном движке + (`rewrite_bytes` generic над `&[Edit]`, пустой span = вставка, `new_text=""` = удаление; + overlap/malformed spans → PlanRejected, не-char-boundary/content-mismatch → StaleIndex + до записи); apply/rollback/diff/json переведены минимально (MovePlan.rewrites → Edit через `From`) +- [x] `Fix` trait рядом с `Language`: `fixes(path, source, index) -> Vec` {rule, severity, auto_fixable, edits}; `jmove fix [--rule ...] [--dry-run] [--json]` -- [ ] Java v1: unused-imports (skip wildcard/ambiguous), import-order (Checkstyle-подобные группы), - missing-import (Type без импорта, ровно 1 кандидат в FQN-индексе → фикс; иначе candidates для агента), - class-name-mismatch + (тот же apply/rollback/diff: `apply_edits` + `render_edits_diff` без move) +- [~] Java v1: unused-imports (DONE, skip wildcard/ambiguous), missing-import (DONE: unique FQN candidate → + insert `import pkg.Type;` at the import-block end; ambiguous/wildcard → `--json` `candidates`, applied:false; + закрыт guava-разрыв «перенесли файл, соседняя ссылка без импорта умерла» — проверено mv+fix+javac SUCCESS), + import-order (DONE: Google-стиль — statics первыми, ASCII-сортировка, дедуп; конфликтующие с другими правилами откладываются (prune_overlaps по severity) и сходятся за 2-3 прогона), class-name-mismatch - [ ] TS v1: unused-imports, import-order; add-import требует индекс экспортов (символ→файл) - [ ] Форматирование: свой cargo-fmt НЕ строим (вечный long-tail). Только «import formatting» (порядок/группировка — у нас уже есть spans). Опционально `--format-after ` (prettier / @@ -69,6 +72,8 @@ AI оставляем СНАРУЖИ: при неоднозначности jmov `GwtCompatible` БЕЗ импорта (тот же пакет) → после mv ссылка битая. jmove в v1 осознанно НЕ добавляет импорты. Это главный driver для fix/missing-import из Phase 1.6 выше - [ ] (после fix) повторить обе перемещения как `mv` + авто-`fix` и добить compile до SUCCESS + (паттерн воспроизведён и закрыт локально: mv файла с bare-ссылкой на соседний пакет → + `fix` добавил импорт → javac SUCCESS; на реальном guava ещё не прогонялось) - [ ] Индексация в monorepo с дублями пакетов (guava vs android/guava в одном --root): FQN-коллизии → class_index оставляет первый по сортировке, импорт резолвится не туда. Нужен выбор/фильтр source root (например `--source-root` или авто-определение по mv-цели) @@ -78,7 +83,7 @@ AI оставляем СНАРУЖИ: при неоднозначности jmov - [ ] Инкрементальная переиндексация (только изменённые файлы) - [ ] Поддержка tsconfig paths / алиасов (@/...) - [ ] Параллельная индексация через rayon -- [ ] --git интеграция (git mv для stage/истории) +- [x] --git интеграция (git mv для stage/истории): auto для tracked файлов, --no-git флаг, moved_via/would_move_via в --json - [ ] Перенос директорий целиком (mv папки) - [ ] Предупреждения о не-import ссылках: package.json exports, jest mocks, tsconfig includes, markdown links - [ ] prettier интеграция после rewrite (по желанию) @@ -87,7 +92,10 @@ AI оставляем СНАРУЖИ: при неоднозначности jmov - [ ] Поддержка Python (from/import, относительные точки) - [ ] Поддержка Go (per-file, НЕ whole-package как refac) - [ ] Команда split (авто-разбивка файла на несколько) -- [ ] Windows-пути (camino/normalize) — CI matrix +- [x] Windows-пути: `core::rel_str` — единый формат относительных путей на границе CLI + (human/JSON/diff-заголовки/git-pathspecs всегда через `/`, не `Path::display()`); + CI matrix linux+windows (`cargo test --locked`), checkout с `core.autocrlf=input`. + Camino не ввели: PathBuf остаётся внутренней валютой, славши нужен только на выводе ## Идеи на потом - [ ] LSP интеграция (jmove сам как LSP server)