1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
//! Reference tracking methods for MagellanIntegration.
use crate::error::{Result, SpliceError};
use std::path::Path;
use super::normalize_lookup_path;
use super::types::*;
use super::MagellanIntegration;
impl MagellanIntegration {
/// Get call relationships for a symbol.
///
/// # Arguments
/// * `file_path` - Path to file containing the symbol
/// * `name` - Symbol name
/// * `direction` - Which relationships to fetch (In/Out/Both)
///
/// # Returns
/// CallRelationships containing the symbol and its relationships.
pub fn get_call_relationships(
&mut self,
file_path: &Path,
name: &str,
direction: CallDirection,
) -> Result<CallRelationships> {
let normalized = normalize_lookup_path(file_path);
let path_str = normalized.to_str().ok_or_else(|| {
SpliceError::Other(format!("Invalid UTF-8 in path: {:?}", normalized))
})?;
// Get the target symbol info first
let symbol_facts = self.inner.symbol_extents(path_str, name).map_err(|e| {
SpliceError::Other(format!(
"Failed to find symbol {} in {}: {}",
name, path_str, e
))
})?;
if symbol_facts.is_empty() {
return Err(SpliceError::Other(format!(
"Symbol '{}' not found in file '{}'",
name, path_str
)));
}
let (entity_id, fact) = &symbol_facts[0];
let target_symbol = SymbolInfo {
entity_id: *entity_id,
name: fact.name.clone().unwrap_or_else(|| name.to_string()),
file_path: fact.file_path.to_string_lossy().to_string(),
kind: fact.kind_normalized.clone(),
byte_start: fact.byte_start,
byte_end: fact.byte_end,
start_line: None,
end_line: None,
};
let (callers, callees) = match direction {
CallDirection::In => {
let calls = self
.inner
.callers_of_symbol(path_str, name)
.map_err(|e| SpliceError::Other(format!("Failed to get callers: {}", e)))?;
(self.resolve_call_facts_to_references(calls)?, Vec::new())
}
CallDirection::Out => {
let calls = self
.inner
.calls_from_symbol(path_str, name)
.map_err(|e| SpliceError::Other(format!("Failed to get callees: {}", e)))?;
(Vec::new(), self.resolve_call_facts_to_references(calls)?)
}
CallDirection::Both => {
let callers_facts = self
.inner
.callers_of_symbol(path_str, name)
.map_err(|e| SpliceError::Other(format!("Failed to get callers: {}", e)))?;
let callees_facts = self
.inner
.calls_from_symbol(path_str, name)
.map_err(|e| SpliceError::Other(format!("Failed to get callees: {}", e)))?;
(
self.resolve_call_facts_to_references(callers_facts)?,
self.resolve_call_facts_to_references(callees_facts)?,
)
}
};
Ok(CallRelationships {
symbol: target_symbol,
callers,
callees,
})
}
/// Resolve CallFact vectors to CallReference vectors with symbol info.
fn resolve_call_facts_to_references(
&mut self,
call_facts: Vec<magellan::references::CallFact>,
) -> Result<Vec<CallReference>> {
let mut references = Vec::new();
let mut seen: std::collections::HashSet<(String, String)> =
std::collections::HashSet::new();
for fact in call_facts {
// Resolve the referenced symbol (caller or callee depending on context)
let ref_name = &fact.callee;
let ref_path_str = fact.file_path.to_string_lossy();
// Get symbol info for the referenced symbol
let symbol_infos = self
.inner
.symbol_extents(&ref_path_str, ref_name)
.map_err(|e| {
SpliceError::Other(format!("Failed to resolve symbol {}: {}", ref_name, e))
})?;
for (entity_id, symbol_fact) in symbol_infos {
let symbol = SymbolInfo {
entity_id,
name: symbol_fact.name.clone().unwrap_or_else(|| ref_name.clone()),
file_path: symbol_fact.file_path.to_string_lossy().to_string(),
kind: symbol_fact.kind_normalized.clone(),
byte_start: symbol_fact.byte_start,
byte_end: symbol_fact.byte_end,
start_line: None,
end_line: None,
};
let key = (symbol.name.clone(), symbol.file_path.clone());
if !seen.insert(key) {
continue;
}
let call_site = CallSite {
file_path: fact.file_path.to_string_lossy().to_string(),
byte_start: fact.byte_start,
byte_end: fact.byte_end,
start_line: fact.start_line,
start_col: fact.start_col,
end_line: fact.end_line,
end_col: fact.end_col,
};
references.push(CallReference { symbol, call_site });
}
}
Ok(references)
}
/// Sort references for safe in-order replacement.
///
/// References are sorted by (file_path, byte_start) with descending
/// byte_start within each file. This ensures that replacing earlier
/// references doesn't affect byte offsets of later ones.
///
/// This is critical when the new name has different byte length.
///
/// # Arguments
/// * `references` - References to sort in-place
pub fn sort_references_for_replacement(references: &mut [magellan::references::ReferenceFact]) {
references.sort_by(|a, b| {
// First by file path (ascending) for logical grouping
match a.file_path.cmp(&b.file_path) {
std::cmp::Ordering::Equal => {
// Then by byte_start within file (descending)
// Descending order prevents offset shifts from affecting later replacements
b.byte_start.cmp(&a.byte_start)
}
other => other,
}
});
}
/// Validate that a byte span is on UTF-8 character boundaries.
///
/// # Arguments
/// * `content` - File content as bytes
/// * `byte_start` - Start offset
/// * `byte_end` - End offset
/// * `file_path` - Path to file (for error reporting)
///
/// # Returns
/// Ok(()) if span is valid, Err with description if invalid
pub fn validate_utf8_span(
content: &[u8],
byte_start: usize,
byte_end: usize,
file_path: &Path,
) -> Result<()> {
if byte_start >= content.len() || byte_end > content.len() {
return Err(SpliceError::InvalidSpan {
file: file_path.to_path_buf(),
start: byte_start,
end: byte_end,
file_size: content.len(),
});
}
// Convert to str for char_boundary checking
// SAFETY: We only validate boundaries, not content validity
let content_str = std::str::from_utf8(content)
.map_err(|_| SpliceError::Other("File content is not valid UTF-8".to_string()))?;
// Check start is on character boundary
if !content_str.is_char_boundary(byte_start) {
return Err(SpliceError::Other(format!(
"Byte start {} is not on UTF-8 character boundary",
byte_start
)));
}
// Check end is on character boundary
if !content_str.is_char_boundary(byte_end) {
return Err(SpliceError::Other(format!(
"Byte end {} is not on UTF-8 character boundary",
byte_end
)));
}
Ok(())
}
/// Get all references for a symbol by entity ID.
///
/// Returns ReferenceFact entries for all references to this symbol
/// across all indexed files. This is the core data for cross-file rename.
///
/// # Arguments
/// * `entity_id` - Entity ID of the symbol definition
///
/// # Returns
/// Vector of ReferenceFact entries with byte spans
pub fn get_all_references(
&mut self,
entity_id: i64,
) -> Result<Vec<magellan::references::ReferenceFact>> {
self.inner.references_to_symbol(entity_id).map_err(|e| {
SpliceError::Other(format!(
"Failed to get references for entity {}: {}",
entity_id, e
))
})
}
/// Index references for a file into the graph.
///
/// This extracts all references to known symbols in the file and creates
/// Reference nodes with REFERENCES edges to the corresponding symbols.
/// This must be called after `index_file` for proper cross-file reference tracking.
///
/// # Arguments
/// * `file_path` - Path to the file to index references for
///
/// # Returns
/// Number of references indexed
///
/// # Errors
/// Returns Other error if indexing fails
pub fn index_references(&mut self, file_path: &Path) -> Result<usize> {
let file_path_str = file_path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 in path: {:?}", file_path)))?;
let source = std::fs::read(file_path).map_err(|e| {
SpliceError::Other(format!("Failed to read file {:?}: {}", file_path, e))
})?;
self.inner
.index_references(file_path_str, &source)
.map_err(|e| {
SpliceError::Other(format!(
"Failed to index references for {:?}: {}",
file_path, e
))
})
}
}