Navegador

navegador / navegador / refactor.py
Blame History Raw 160 lines
1
"""
2
Coordinated rename — graph-assisted multi-file symbol refactoring.
3
4
Usage:
5
from navegador.refactor import SymbolRenamer
6
7
renamer = SymbolRenamer(store)
8
preview = renamer.preview_rename("old_name", "new_name")
9
print(preview.affected_files)
10
result = renamer.apply_rename("old_name", "new_name")
11
"""
12
13
from __future__ import annotations
14
15
import logging
16
from dataclasses import dataclass, field
17
from typing import Any
18
19
from navegador.graph.store import GraphStore
20
21
logger = logging.getLogger(__name__)
22
23
24
# ── Data models ───────────────────────────────────────────────────────────────
25
26
27
@dataclass
28
class RenamePreview:
29
"""Shows what would change if the rename were applied."""
30
31
old_name: str
32
new_name: str
33
affected_files: list[str] = field(default_factory=list)
34
affected_nodes: list[dict[str, Any]] = field(default_factory=list)
35
edges_updated: int = 0
36
37
38
@dataclass
39
class RenameResult:
40
"""Records what actually changed after applying the rename."""
41
42
old_name: str
43
new_name: str
44
affected_files: list[str] = field(default_factory=list)
45
affected_nodes: list[dict[str, Any]] = field(default_factory=list)
46
edges_updated: int = 0
47
48
49
# ── Core class ────────────────────────────────────────────────────────────────
50
51
52
class SymbolRenamer:
53
"""
54
Graph-assisted multi-file symbol refactoring.
55
56
Operates entirely on the graph: it finds nodes whose ``name`` matches the
57
symbol and updates them in place. It does *not* edit source files on disk
58
(that is left to the editor / agent layer).
59
"""
60
61
def __init__(self, store: GraphStore) -> None:
62
self.store = store
63
64
# ── Public API ────────────────────────────────────────────────────────────
65
66
def find_references(self, name: str, file_path: str = "") -> list[dict[str, Any]]:
67
"""
68
Return all graph nodes whose name matches *name*.
69
70
Optionally filter to a specific file with *file_path*.
71
"""
72
if file_path:
73
cypher = (
74
"MATCH (n) "
75
"WHERE n.name = $name AND n.file_path = $fp "
76
"RETURN labels(n)[0] AS label, n.name AS name, "
77
" coalesce(n.file_path, '') AS file_path, "
78
" coalesce(n.line_start, 0) AS line_start"
79
)
80
result = self.store.query(cypher, {"name": name, "fp": file_path})
81
else:
82
cypher = (
83
"MATCH (n) "
84
"WHERE n.name = $name "
85
"RETURN labels(n)[0] AS label, n.name AS name, "
86
" coalesce(n.file_path, '') AS file_path, "
87
" coalesce(n.line_start, 0) AS line_start"
88
)
89
result = self.store.query(cypher, {"name": name})
90
91
rows = result.result_set or []
92
return [
93
{
94
"label": row[0],
95
"name": row[1],
96
"file_path": row[2],
97
"line_start": row[3],
98
}
99
for row in rows
100
]
101
102
def preview_rename(self, old_name: str, new_name: str) -> RenamePreview:
103
"""
104
Return a RenamePreview showing what would change without modifying
105
anything.
106
"""
107
refs = self.find_references(old_name)
108
affected_files = sorted({r["file_path"] for r in refs if r["file_path"]})
109
110
# Count edges that touch these nodes
111
edges_updated = self._count_edges(old_name)
112
113
return RenamePreview(
114
old_name=old_name,
115
new_name=new_name,
116
affected_files=affected_files,
117
affected_nodes=refs,
118
edges_updated=edges_updated,
119
)
120
121
def apply_rename(self, old_name: str, new_name: str) -> RenameResult:
122
"""
123
Update all graph nodes named *old_name* to *new_name*.
124
125
Returns a RenameResult describing what was changed.
126
"""
127
refs = self.find_references(old_name)
128
affected_files = sorted({r["file_path"] for r in refs if r["file_path"]})
129
edges_updated = self._count_edges(old_name)
130
131
# Update every node whose name matches
132
cypher = "MATCH (n) WHERE n.name = $old SET n.name = $new"
133
self.store.query(cypher, {"old": old_name, "new": new_name})
134
logger.info(
135
"SymbolRenamer: renamed %r → %r (%d nodes, %d edges)",
136
old_name,
137
new_name,
138
len(refs),
139
edges_updated,
140
)
141
142
return RenameResult(
143
old_name=old_name,
144
new_name=new_name,
145
affected_files=affected_files,
146
affected_nodes=refs,
147
edges_updated=edges_updated,
148
)
149
150
# ── Helpers ───────────────────────────────────────────────────────────────
151
152
def _count_edges(self, name: str) -> int:
153
"""Count edges incident on nodes named *name*."""
154
cypher = "MATCH (n)-[r]-() WHERE n.name = $name RETURN count(r) AS c"
155
result = self.store.query(cypher, {"name": name})
156
rows = result.result_set or []
157
if rows:
158
return rows[0][0] or 0
159
return 0
160

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button