169 lines
5.6 KiB
Python
169 lines
5.6 KiB
Python
"""Tools 17, 18: refactor_func, apply_refactor_func."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ..hints import generate_hints, get_session
|
|
from ..incremental import find_project_root
|
|
from ..refactor import (
|
|
apply_refactor,
|
|
find_dead_code,
|
|
rename_preview,
|
|
suggest_refactorings,
|
|
)
|
|
from ._common import _get_store, _validate_repo_root
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool 17: refactor_tool [REFACTOR]
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def refactor_func(
|
|
mode: str = "rename",
|
|
old_name: str | None = None,
|
|
new_name: str | None = None,
|
|
kind: str | None = None,
|
|
file_pattern: str | None = None,
|
|
repo_root: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Unified refactoring entry point.
|
|
|
|
[REFACTOR] Supports three modes:
|
|
- ``rename``: Preview renaming a symbol (requires *old_name* and
|
|
*new_name*).
|
|
- ``dead_code``: Find unreferenced functions/classes.
|
|
- ``suggest``: Get community-driven refactoring suggestions.
|
|
|
|
Args:
|
|
mode: One of ``"rename"``, ``"dead_code"``, or ``"suggest"``.
|
|
old_name: (rename mode) Current symbol name.
|
|
new_name: (rename mode) Desired new name.
|
|
kind: (dead_code mode) Optional node kind filter.
|
|
file_pattern: (dead_code mode) Optional file path substring filter.
|
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
|
|
Returns:
|
|
Mode-specific results dict.
|
|
"""
|
|
valid_modes = {"rename", "dead_code", "suggest"}
|
|
if mode not in valid_modes:
|
|
return {
|
|
"status": "error",
|
|
"error": (
|
|
f"Invalid mode '{mode}'. "
|
|
f"Must be one of: {', '.join(sorted(valid_modes))}"
|
|
),
|
|
}
|
|
|
|
store, root = _get_store(repo_root)
|
|
try:
|
|
if mode == "rename":
|
|
if not old_name or not new_name:
|
|
return {
|
|
"status": "error",
|
|
"error": (
|
|
"rename mode requires both old_name and new_name."
|
|
),
|
|
}
|
|
preview = rename_preview(store, old_name, new_name)
|
|
if preview is None:
|
|
return {
|
|
"status": "not_found",
|
|
"summary": f"No node found matching '{old_name}'.",
|
|
}
|
|
result = {
|
|
"status": "ok",
|
|
"summary": (
|
|
f"Rename preview: {old_name} -> {new_name}, "
|
|
f"{len(preview['edits'])} edit(s). "
|
|
f"Use apply_refactor_tool(refactor_id="
|
|
f"'{preview['refactor_id']}') to apply."
|
|
),
|
|
**preview,
|
|
}
|
|
result["_hints"] = generate_hints(
|
|
"refactor", result, get_session()
|
|
)
|
|
return result
|
|
|
|
elif mode == "dead_code":
|
|
dead = find_dead_code(
|
|
store, kind=kind, file_pattern=file_pattern, root=root
|
|
)
|
|
result = {
|
|
"status": "ok",
|
|
"summary": f"Found {len(dead)} dead code symbol(s).",
|
|
"dead_code": dead,
|
|
"total": len(dead),
|
|
}
|
|
result["_hints"] = generate_hints(
|
|
"refactor", result, get_session()
|
|
)
|
|
return result
|
|
|
|
else: # suggest
|
|
suggestions = suggest_refactorings(store)
|
|
result = {
|
|
"status": "ok",
|
|
"summary": (
|
|
f"Generated {len(suggestions)} "
|
|
"refactoring suggestion(s)."
|
|
),
|
|
"suggestions": suggestions,
|
|
"total": len(suggestions),
|
|
}
|
|
result["_hints"] = generate_hints(
|
|
"refactor", result, get_session()
|
|
)
|
|
return result
|
|
|
|
except Exception as exc:
|
|
return {"status": "error", "error": str(exc)}
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool 18: apply_refactor_tool [REFACTOR]
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def apply_refactor_func(
|
|
refactor_id: str,
|
|
repo_root: str | None = None,
|
|
dry_run: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Apply a previously previewed refactoring to source files.
|
|
|
|
[REFACTOR] Validates the refactor_id, checks expiry, ensures all edit
|
|
paths are within the repo root, then performs exact string replacements.
|
|
|
|
Args:
|
|
refactor_id: ID returned by a prior ``refactor_tool(mode="rename")``
|
|
call.
|
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
dry_run: If True, return a unified diff of what would change
|
|
without touching disk. The refactor_id remains valid so the
|
|
user can review the diff, then call again with ``dry_run=False``
|
|
to actually write the changes. See: #176
|
|
|
|
Returns:
|
|
Status with count of applied edits and modified files. When
|
|
``dry_run=True`` the response additionally contains ``would_modify``
|
|
(list of file paths) and ``diffs`` (map of file -> unified-diff
|
|
string).
|
|
"""
|
|
try:
|
|
root = (
|
|
_validate_repo_root(Path(repo_root))
|
|
if repo_root
|
|
else find_project_root()
|
|
)
|
|
except (RuntimeError, ValueError) as exc:
|
|
return {"status": "error", "error": str(exc)}
|
|
|
|
result = apply_refactor(refactor_id, root, dry_run=dry_run)
|
|
return result
|