How the Refactor Tool Previews and Applies Renames in code-review-graph
The refactor tool implements a thread-safe preview-then-apply workflow where rename_preview generates an atomic edit plan stored in memory, and apply_refactor executes or dry-runs those changes with path-traversal protection and automatic expiry cleanup.
The tirth8205/code-review-graph repository provides a static-analysis-driven refactoring engine for Python codebases. Its refactor tool enables safe, auditable renaming of symbols by combining graph-based dependency tracking with atomic file operations and concurrency controls.
Thread-Safe Preview Storage Architecture
The refactoring workflow centers on a global registry that maintains pending operations while preventing race conditions. According to the source code in code_review_graph/refactor.py, the system uses:
_pending_refactors: A global dictionary that holds pending refactor previews (lines 51-55)._refactor_lock: Athreading.Lockinstance that guards all access to the pending dictionary._cleanup_expired: A background routine that removes previews older thanREFACTOR_EXPIRY_SECONDS(10 minutes).
This architecture ensures that multiple threads can safely generate and apply renames without corrupting the preview state.
Generating Rename Previews with rename_preview
The rename_preview function (lines 73-78) initiates the workflow by analyzing the code graph to build a complete list of required edits.
Locating Symbol Definitions and References
The function accepts a GraphStore instance, the old_name symbol, and the desired new_name. It locates the target node via store.search_nodes, preferring an exact name match but falling back to the first candidate if necessary.
Building the Edit List
Once the symbol is identified, the tool collects four categories of modification sites:
- Definition sites: The node itself where the symbol is defined.
- Call sites: Edges of kind
CALLStargeting the symbol. - Bare-name call sites: Direct invocations without module qualification.
- Import sites: Edges of kind
IMPORTS_FROMreferencing the symbol.
Each site is converted into a dictionary describing the file path, line number, old text, new text, and confidence level. A unique refactor_id (generated via uuid.uuid4().hex[:8]) tags the complete preview, which is then stored in _pending_refactors before being returned to the caller.
Applying Renames with apply_refactor
The apply_refactor function (lines 702-78) consumes the stored preview to modify files or generate diffs.
Safety Validation and Path Protection
Before touching any files, the function validates the refactor_id against the registry, checking both existence and expiry. It then implements path-traversal protection: each edit’s target file is resolved and verified to lie within the specified repo_root. If any file attempts to escape the repository boundary, the operation aborts immediately.
Grouping and Executing Edits
Edits are grouped by file to handle multiple replacements within a single document efficiently. For each file, the tool applies edits sequentially against the evolving content, ensuring that line number shifts from earlier edits do not corrupt later replacements.
Dry-Run Mode and Diff Generation
When dry_run=True, the function uses difflib.unified_diff to generate line-level diffs without writing to disk. This mode preserves the preview in _pending_refactors, allowing developers to inspect changes before committing. In non-dry-run mode, the new content is written back to each file, edit counts are tallied, and the preview entry is atomically removed from the registry.
Complete Workflow Example
The following demonstrates the full preview-and-apply lifecycle:
from pathlib import Path
from code_review_graph.refactor import rename_preview, apply_refactor
# 1️⃣ Generate a preview for renaming `old_func` → `new_func`
preview = rename_preview(store, "old_func", "new_func")
print(preview["refactor_id"], preview["edits"])
# 2️⃣ Inspect the preview (dry‑run) – see diffs without touching files
dry_result = apply_refactor(
preview["refactor_id"],
repo_root=Path("/path/to/repo"),
dry_run=True,
)
print(dry_result["diffs"]["some/file.py"])
# 3️⃣ Actually apply the rename
apply_result = apply_refactor(
preview["refactor_id"],
repo_root=Path("/path/to/repo"),
dry_run=False,
)
print(apply_result["files_modified"])
Summary
- Thread-safe storage: A global locked dictionary
_pending_refactorsstores previews for 10 minutes, guarded by_refactor_lock(lines 51-55). - Graph-driven analysis:
rename_previewusesGraphStore.search_nodesand edge traversal to collect definition, call, and import sites, generating a unique ID for each operation (lines 73-78). - Safety mechanisms: Path-traversal validation, expiry enforcement, and atomic file grouping prevent unsafe or partial updates.
- Dry-run support:
apply_refactorgenerates unified diffs viadifflib.unified_diffwhendry_run=True, enabling inspection before permanent writes.
Frequently Asked Questions
How does the refactor tool prevent stale previews from being applied?
The tool enforces a 10-minute expiry window via REFACTOR_EXPIRY_SECONDS and the _cleanup_expired routine. Before applying any changes, apply_refactor validates that the refactor_id exists in _pending_refactors and has not exceeded the expiry limit, returning an error if the preview is missing or outdated.
What protects against directory traversal attacks when applying renames?
The apply_refactor function in code_review_graph/refactor.py resolves each edit’s target file path and verifies that it lies within the specified repo_root directory. If any file path attempts to escape the repository root, the operation aborts immediately to prevent directory-traversal vulnerabilities.
Can I preview changes before writing them to disk?
Yes. By passing dry_run=True to apply_refactor, the tool generates unified diffs using difflib.unified_diff for each affected file without persisting changes. This preserves the preview in _pending_refactors for a subsequent non-dry-run application.
How does the tool handle concurrent rename operations?
Access to the global _pending_refactors dictionary is synchronized via _refactor_lock, a threading.Lock instance defined in code_review_graph/refactor.py. This ensures that only one thread manipulates pending previews at a time, preventing race conditions during both preview generation and application.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →