1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Location, Position, Range, Url};
4
5#[derive(Debug, Clone)]
6pub struct NodeInfo {
7 pub src: String,
8 pub name_location: Option<String>,
9 pub name_locations: Vec<String>,
10 pub referenced_declaration: Option<u64>,
11 pub node_type: Option<String>,
12 pub member_location: Option<String>,
13 pub absolute_path: Option<String>,
14}
15
16pub const CHILD_KEYS: &[&str] = &[
18 "AST",
19 "arguments",
20 "baseContracts",
21 "baseExpression",
22 "baseName",
23 "baseType",
24 "block",
25 "body",
26 "components",
27 "condition",
28 "declarations",
29 "endExpression",
30 "errorCall",
31 "eventCall",
32 "expression",
33 "externalCall",
34 "falseBody",
35 "falseExpression",
36 "file",
37 "foreign",
38 "functionName",
39 "indexExpression",
40 "initialValue",
41 "initializationExpression",
42 "keyType",
43 "leftExpression",
44 "leftHandSide",
45 "libraryName",
46 "literals",
47 "loopExpression",
48 "members",
49 "modifierName",
50 "modifiers",
51 "name",
52 "names",
53 "nodes",
54 "options",
55 "overrides",
56 "parameters",
57 "pathNode",
58 "post",
59 "pre",
60 "returnParameters",
61 "rightExpression",
62 "rightHandSide",
63 "startExpression",
64 "statements",
65 "storageLayout",
66 "subExpression",
67 "subdenomination",
68 "symbolAliases",
69 "trueBody",
70 "trueExpression",
71 "typeName",
72 "unitAlias",
73 "value",
74 "valueType",
75 "variableNames",
76 "variables",
77];
78
79fn push_if_node_or_array<'a>(tree: &'a Value, key: &str, stack: &mut Vec<&'a Value>) {
80 if let Some(value) = tree.get(key) {
81 match value {
82 Value::Array(arr) => {
83 stack.extend(arr);
84 }
85 Value::Object(_) => {
86 stack.push(value);
87 }
88 _ => {}
89 }
90 }
91}
92
93pub type ExternalRefs = HashMap<String, u64>;
96
97#[derive(Debug, Clone)]
100pub struct CachedBuild {
101 pub ast: Value,
102 pub nodes: HashMap<String, HashMap<u64, NodeInfo>>,
103 pub path_to_abs: HashMap<String, String>,
104 pub external_refs: ExternalRefs,
105 pub id_to_path_map: HashMap<String, String>,
106}
107
108impl CachedBuild {
109 pub fn new(ast: Value) -> Self {
111 let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
112 cache_ids(sources)
113 } else {
114 (HashMap::new(), HashMap::new(), HashMap::new())
115 };
116
117 let id_to_path_map = ast
118 .get("build_infos")
119 .and_then(|v| v.as_array())
120 .and_then(|arr| arr.first())
121 .and_then(|info| info.get("source_id_to_path"))
122 .and_then(|v| v.as_object())
123 .map(|obj| {
124 obj.iter()
125 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
126 .collect()
127 })
128 .unwrap_or_default();
129
130 Self {
131 ast,
132 nodes,
133 path_to_abs,
134 external_refs,
135 id_to_path_map,
136 }
137 }
138}
139
140type Type = (
141 HashMap<String, HashMap<u64, NodeInfo>>,
142 HashMap<String, String>,
143 ExternalRefs,
144);
145
146pub fn cache_ids(sources: &Value) -> Type {
147 let mut nodes: HashMap<String, HashMap<u64, NodeInfo>> = HashMap::new();
148 let mut path_to_abs: HashMap<String, String> = HashMap::new();
149 let mut external_refs: ExternalRefs = HashMap::new();
150
151 if let Some(sources_obj) = sources.as_object() {
152 for (path, contents) in sources_obj {
153 if let Some(contents_array) = contents.as_array()
154 && let Some(first_content) = contents_array.first()
155 && let Some(source_file) = first_content.get("source_file")
156 && let Some(ast) = source_file.get("ast")
157 {
158 let abs_path = ast
160 .get("absolutePath")
161 .and_then(|v| v.as_str())
162 .unwrap_or(path)
163 .to_string();
164
165 path_to_abs.insert(path.clone(), abs_path.clone());
166
167 if !nodes.contains_key(&abs_path) {
169 nodes.insert(abs_path.clone(), HashMap::new());
170 }
171
172 if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
173 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
174 {
175 nodes.get_mut(&abs_path).unwrap().insert(
176 id,
177 NodeInfo {
178 src: src.to_string(),
179 name_location: None,
180 name_locations: vec![],
181 referenced_declaration: None,
182 node_type: ast
183 .get("nodeType")
184 .and_then(|v| v.as_str())
185 .map(|s| s.to_string()),
186 member_location: None,
187 absolute_path: ast
188 .get("absolutePath")
189 .and_then(|v| v.as_str())
190 .map(|s| s.to_string()),
191 },
192 );
193 }
194
195 let mut stack = vec![ast];
196
197 while let Some(tree) = stack.pop() {
198 if let Some(id) = tree.get("id").and_then(|v| v.as_u64())
199 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
200 {
201 let mut name_location = tree
203 .get("nameLocation")
204 .and_then(|v| v.as_str())
205 .map(|s| s.to_string());
206
207 if name_location.is_none()
211 && let Some(name_locations) = tree.get("nameLocations")
212 && let Some(locations_array) = name_locations.as_array()
213 && !locations_array.is_empty()
214 {
215 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
216 if node_type == Some("IdentifierPath") {
217 name_location = locations_array
218 .last()
219 .and_then(|v| v.as_str())
220 .map(|s| s.to_string());
221 } else {
222 name_location = locations_array[0].as_str().map(|s| s.to_string());
223 }
224 }
225
226 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
227 && let Some(locations_array) = name_locations.as_array()
228 {
229 locations_array
230 .iter()
231 .filter_map(|v| v.as_str().map(|s| s.to_string()))
232 .collect()
233 } else {
234 vec![]
235 };
236
237 let mut final_name_location = name_location;
238 if final_name_location.is_none()
239 && let Some(member_loc) =
240 tree.get("memberLocation").and_then(|v| v.as_str())
241 {
242 final_name_location = Some(member_loc.to_string());
243 }
244
245 let node_info = NodeInfo {
246 src: src.to_string(),
247 name_location: final_name_location,
248 name_locations,
249 referenced_declaration: tree
250 .get("referencedDeclaration")
251 .and_then(|v| v.as_u64()),
252 node_type: tree
253 .get("nodeType")
254 .and_then(|v| v.as_str())
255 .map(|s| s.to_string()),
256 member_location: tree
257 .get("memberLocation")
258 .and_then(|v| v.as_str())
259 .map(|s| s.to_string()),
260 absolute_path: tree
261 .get("absolutePath")
262 .and_then(|v| v.as_str())
263 .map(|s| s.to_string()),
264 };
265
266 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
267
268 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
270 && let Some(ext_refs) =
271 tree.get("externalReferences").and_then(|v| v.as_array())
272 {
273 for ext_ref in ext_refs {
274 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
275 && let Some(decl_id) =
276 ext_ref.get("declaration").and_then(|v| v.as_u64())
277 {
278 external_refs.insert(src_str.to_string(), decl_id);
279 }
280 }
281 }
282 }
283
284 for key in CHILD_KEYS {
285 push_if_node_or_array(tree, key, &mut stack);
286 }
287 }
288 }
289 }
290 }
291
292 (nodes, path_to_abs, external_refs)
293}
294
295pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
296 let text = String::from_utf8_lossy(source_bytes);
297 crate::utils::position_to_byte_offset(&text, position.line, position.character)
298}
299
300pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
301 let text = String::from_utf8_lossy(source_bytes);
302 let (line, col) = crate::utils::byte_offset_to_position(&text, byte_offset);
303 Some(Position::new(line, col))
304}
305
306pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
308 let parts: Vec<&str> = src.split(':').collect();
309 if parts.len() != 3 {
310 return None;
311 }
312 let byte_offset: usize = parts[0].parse().ok()?;
313 let length: usize = parts[1].parse().ok()?;
314 let file_id = parts[2];
315 let file_path = id_to_path.get(file_id)?;
316
317 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
318 std::path::PathBuf::from(file_path)
319 } else {
320 std::env::current_dir().ok()?.join(file_path)
321 };
322
323 let source_bytes = std::fs::read(&absolute_path).ok()?;
324 let start_pos = bytes_to_pos(&source_bytes, byte_offset)?;
325 let end_pos = bytes_to_pos(&source_bytes, byte_offset + length)?;
326 let uri = Url::from_file_path(&absolute_path).ok()?;
327
328 Some(Location {
329 uri,
330 range: Range {
331 start: start_pos,
332 end: end_pos,
333 },
334 })
335}
336
337pub fn goto_bytes(
338 nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
339 path_to_abs: &HashMap<String, String>,
340 id_to_path: &HashMap<String, String>,
341 external_refs: &ExternalRefs,
342 uri: &str,
343 position: usize,
344) -> Option<(String, usize, usize)> {
345 let path = match uri.starts_with("file://") {
346 true => &uri[7..],
347 false => uri,
348 };
349
350 let abs_path = path_to_abs.get(path)?;
352
353 let current_file_nodes = nodes.get(abs_path)?;
355
356 let path_to_file_id: HashMap<&str, &str> = id_to_path
358 .iter()
359 .map(|(id, p)| (p.as_str(), id.as_str()))
360 .collect();
361
362 let current_file_id = path_to_file_id.get(abs_path.as_str());
366
367 for (src_str, decl_id) in external_refs {
369 let src_parts: Vec<&str> = src_str.split(':').collect();
370 if src_parts.len() != 3 {
371 continue;
372 }
373
374 if let Some(file_id) = current_file_id {
376 if src_parts[2] != *file_id {
377 continue;
378 }
379 } else {
380 continue;
381 }
382
383 let start_b: usize = src_parts[0].parse().ok()?;
384 let length: usize = src_parts[1].parse().ok()?;
385 let end_b = start_b + length;
386
387 if start_b <= position && position < end_b {
388 let mut target_node: Option<&NodeInfo> = None;
390 for file_nodes in nodes.values() {
391 if let Some(node) = file_nodes.get(decl_id) {
392 target_node = Some(node);
393 break;
394 }
395 }
396 let node = target_node?;
397 let (location_str, length_str, file_id) =
398 if let Some(name_location) = &node.name_location {
399 let parts: Vec<&str> = name_location.split(':').collect();
400 if parts.len() == 3 {
401 (parts[0], parts[1], parts[2])
402 } else {
403 return None;
404 }
405 } else {
406 let parts: Vec<&str> = node.src.split(':').collect();
407 if parts.len() == 3 {
408 (parts[0], parts[1], parts[2])
409 } else {
410 return None;
411 }
412 };
413 let location: usize = location_str.parse().ok()?;
414 let len: usize = length_str.parse().ok()?;
415 let file_path = id_to_path.get(file_id)?.clone();
416 return Some((file_path, location, len));
417 }
418 }
419
420 let mut refs = HashMap::new();
421
422 for (id, content) in current_file_nodes {
424 if content.referenced_declaration.is_none() {
425 continue;
426 }
427
428 let src_parts: Vec<&str> = content.src.split(':').collect();
429 if src_parts.len() != 3 {
430 continue;
431 }
432
433 let start_b: usize = src_parts[0].parse().ok()?;
434 let length: usize = src_parts[1].parse().ok()?;
435 let end_b = start_b + length;
436
437 if start_b <= position && position < end_b {
438 let diff = end_b - start_b;
439 if !refs.contains_key(&diff) || refs[&diff] <= *id {
440 refs.insert(diff, *id);
441 }
442 }
443 }
444
445 if refs.is_empty() {
446 let tmp = current_file_nodes.iter();
449 for (_id, content) in tmp {
450 if content.node_type == Some("ImportDirective".to_string()) {
451 let src_parts: Vec<&str> = content.src.split(':').collect();
452 if src_parts.len() != 3 {
453 continue;
454 }
455
456 let start_b: usize = src_parts[0].parse().ok()?;
457 let length: usize = src_parts[1].parse().ok()?;
458 let end_b = start_b + length;
459
460 if start_b <= position
461 && position < end_b
462 && let Some(import_path) = &content.absolute_path
463 {
464 return Some((import_path.clone(), 0, 0));
465 }
466 }
467 }
468 return None;
469 }
470
471 let min_diff = *refs.keys().min()?;
473 let chosen_id = refs[&min_diff];
474 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
475
476 let mut target_node: Option<&NodeInfo> = None;
478 for file_nodes in nodes.values() {
479 if let Some(node) = file_nodes.get(&ref_id) {
480 target_node = Some(node);
481 break;
482 }
483 }
484
485 let node = target_node?;
486
487 let (location_str, length_str, file_id) = if let Some(name_location) = &node.name_location {
489 let parts: Vec<&str> = name_location.split(':').collect();
490 if parts.len() == 3 {
491 (parts[0], parts[1], parts[2])
492 } else {
493 return None;
494 }
495 } else {
496 let parts: Vec<&str> = node.src.split(':').collect();
497 if parts.len() == 3 {
498 (parts[0], parts[1], parts[2])
499 } else {
500 return None;
501 }
502 };
503
504 let location: usize = location_str.parse().ok()?;
505 let len: usize = length_str.parse().ok()?;
506 let file_path = id_to_path.get(file_id)?.clone();
507
508 Some((file_path, location, len))
509}
510
511pub fn goto_declaration(
512 ast_data: &Value,
513 file_uri: &Url,
514 position: Position,
515 source_bytes: &[u8],
516) -> Option<Location> {
517 let sources = ast_data.get("sources")?;
518 let build_infos = ast_data.get("build_infos")?.as_array()?;
519 let first_build_info = build_infos.first()?;
520 let id_to_path = first_build_info.get("source_id_to_path")?.as_object()?;
521
522 let id_to_path_map: HashMap<String, String> = id_to_path
523 .iter()
524 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
525 .collect();
526
527 let (nodes, path_to_abs, external_refs) = cache_ids(sources);
528 let byte_position = pos_to_bytes(source_bytes, position);
529
530 if let Some((file_path, location_bytes, length)) = goto_bytes(
531 &nodes,
532 &path_to_abs,
533 &id_to_path_map,
534 &external_refs,
535 file_uri.as_ref(),
536 byte_position,
537 ) {
538 let target_file_path = std::path::Path::new(&file_path);
539 let absolute_path = if target_file_path.is_absolute() {
540 target_file_path.to_path_buf()
541 } else {
542 std::env::current_dir().ok()?.join(target_file_path)
543 };
544
545 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
546 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
547 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
548 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
549 {
550 return Some(Location {
551 uri: target_uri,
552 range: Range {
553 start: start_pos,
554 end: end_pos,
555 },
556 });
557 }
558 };
559
560 None
561}