solidity_language_server/
gas.rs1use serde_json::Value;
8use std::collections::HashMap;
9
10use crate::types::{FuncSelector, MethodId};
11
12pub const GAS_SENTINEL: &str = "@custom:lsp-enable gas-estimates";
15
16#[derive(Debug, Clone, Default)]
18pub struct ContractGas {
19 pub creation: HashMap<String, String>,
21 pub external_by_selector: HashMap<FuncSelector, String>,
23 pub external_by_sig: HashMap<MethodId, String>,
25 pub internal: HashMap<String, String>,
27}
28
29pub type GasIndex = HashMap<String, ContractGas>;
31
32pub fn build_gas_index(ast_data: &Value) -> GasIndex {
40 let mut index = GasIndex::new();
41
42 let contracts = match ast_data.get("contracts").and_then(|c| c.as_object()) {
43 Some(c) => c,
44 None => return index,
45 };
46
47 for (path, names) in contracts {
48 let names_obj = match names.as_object() {
49 Some(n) => n,
50 None => continue,
51 };
52
53 for (name, contract) in names_obj {
54 let evm = match contract.get("evm") {
55 Some(e) => e,
56 None => continue,
57 };
58
59 let gas_estimates = match evm.get("gasEstimates") {
60 Some(g) => g,
61 None => continue,
62 };
63
64 let mut contract_gas = ContractGas::default();
65
66 if let Some(creation) = gas_estimates.get("creation").and_then(|c| c.as_object()) {
68 for (key, value) in creation {
69 let cost = value.as_str().unwrap_or("").to_string();
70 contract_gas.creation.insert(key.clone(), cost);
71 }
72 }
73
74 let method_ids = evm.get("methodIdentifiers").and_then(|m| m.as_object());
76
77 if let Some(external) = gas_estimates.get("external").and_then(|e| e.as_object()) {
78 let sig_to_selector: HashMap<&str, &str> = method_ids
80 .map(|mi| {
81 mi.iter()
82 .filter_map(|(sig, sel)| sel.as_str().map(|s| (sig.as_str(), s)))
83 .collect()
84 })
85 .unwrap_or_default();
86
87 for (sig, value) in external {
88 let cost = value.as_str().unwrap_or("").to_string();
89 if let Some(selector) = sig_to_selector.get(sig.as_str()) {
91 contract_gas
92 .external_by_selector
93 .insert(FuncSelector::new(*selector), cost.clone());
94 }
95 contract_gas
97 .external_by_sig
98 .insert(MethodId::new(sig.clone()), cost);
99 }
100 }
101
102 if let Some(internal) = gas_estimates.get("internal").and_then(|i| i.as_object()) {
104 for (sig, value) in internal {
105 let cost = value.as_str().unwrap_or("").to_string();
106 contract_gas.internal.insert(sig.clone(), cost);
107 }
108 }
109
110 let key = format!("{path}:{name}");
111 index.insert(key, contract_gas);
112 }
113 }
114
115 index
116}
117
118pub fn gas_by_selector<'a>(
120 index: &'a GasIndex,
121 selector: &FuncSelector,
122) -> Option<(&'a str, &'a str)> {
123 for (contract_key, gas) in index {
124 if let Some(cost) = gas.external_by_selector.get(selector) {
125 return Some((contract_key.as_str(), cost.as_str()));
126 }
127 }
128 None
129}
130
131pub fn gas_by_name<'a>(index: &'a GasIndex, name: &str) -> Vec<(&'a str, &'a str, &'a str)> {
135 let prefix = format!("{name}(");
136 let mut results = Vec::new();
137 for (contract_key, gas) in index {
138 for (sig, cost) in &gas.internal {
139 if sig.starts_with(&prefix) {
140 results.push((contract_key.as_str(), sig.as_str(), cost.as_str()));
141 }
142 }
143 }
144 results
145}
146
147pub fn gas_for_contract<'a>(
149 index: &'a GasIndex,
150 path: &str,
151 name: &str,
152) -> Option<&'a ContractGas> {
153 let key = format!("{path}:{name}");
154 index.get(&key)
155}
156
157pub fn resolve_contract_key(
163 sources: &Value,
164 decl_node: &Value,
165 index: &GasIndex,
166) -> Option<String> {
167 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
168
169 let (contract_name, source_id) = if node_type == "ContractDefinition" {
171 let name = decl_node.get("name").and_then(|v| v.as_str())?;
172 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
173 (name.to_string(), scope_id)
174 } else {
175 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
177 let scope_node = crate::hover::find_node_by_id(sources, crate::types::NodeId(scope_id))?;
178 let contract_name = scope_node.get("name").and_then(|v| v.as_str())?;
179 let source_id = scope_node.get("scope").and_then(|v| v.as_u64())?;
180 (contract_name.to_string(), source_id)
181 };
182
183 let source_unit = crate::hover::find_node_by_id(sources, crate::types::NodeId(source_id))?;
185 let abs_path = source_unit.get("absolutePath").and_then(|v| v.as_str())?;
186
187 let exact_key = format!("{abs_path}:{contract_name}");
189 if index.contains_key(&exact_key) {
190 return Some(exact_key);
191 }
192
193 let file_name = std::path::Path::new(abs_path).file_name()?.to_str()?;
196 let suffix = format!("{file_name}:{contract_name}");
197 index.keys().find(|k| k.ends_with(&suffix)).cloned()
198}
199
200pub fn format_gas(cost: &str) -> String {
204 if cost == "infinite" {
205 return "infinite".to_string();
206 }
207 if let Ok(n) = cost.parse::<u64>() {
209 let s = n.to_string();
210 let mut result = String::new();
211 for (i, c) in s.chars().rev().enumerate() {
212 if i > 0 && i % 3 == 0 {
213 result.push(',');
214 }
215 result.push(c);
216 }
217 result.chars().rev().collect()
218 } else {
219 cost.to_string()
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use serde_json::json;
227
228 fn load_solc_fixture() -> Value {
230 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
231 let raw: Value = serde_json::from_str(&data).expect("valid json");
232 crate::solc::normalize_solc_output(raw, None)
233 }
234
235 fn load_forge_fixture() -> Value {
237 let data = std::fs::read_to_string("pool-manager-ast.json").expect("test fixture");
238 let raw: Value = serde_json::from_str(&data).expect("valid json");
239 crate::solc::normalize_forge_output(raw)
240 }
241
242 #[test]
243 fn test_format_gas_number() {
244 assert_eq!(format_gas("109"), "109");
245 assert_eq!(format_gas("2595"), "2,595");
246 assert_eq!(format_gas("6924600"), "6,924,600");
247 assert_eq!(format_gas("28088"), "28,088");
248 }
249
250 #[test]
251 fn test_format_gas_infinite() {
252 assert_eq!(format_gas("infinite"), "infinite");
253 }
254
255 #[test]
256 fn test_format_gas_unknown() {
257 assert_eq!(format_gas("unknown"), "unknown");
258 }
259
260 #[test]
261 fn test_build_gas_index_empty() {
262 let data = json!({});
263 let index = build_gas_index(&data);
264 assert!(index.is_empty());
265 }
266
267 #[test]
268 fn test_build_gas_index_no_contracts() {
269 let data = json!({ "sources": {}, "contracts": {} });
270 let index = build_gas_index(&data);
271 assert!(index.is_empty());
272 }
273
274 #[test]
275 fn test_build_gas_index_basic() {
276 let data = json!({
277 "contracts": {
278 "src/Foo.sol": {
279 "Foo": {
280 "evm": {
281 "gasEstimates": {
282 "creation": {
283 "codeDepositCost": "200",
284 "executionCost": "infinite",
285 "totalCost": "infinite"
286 },
287 "external": {
288 "bar(uint256)": "109"
289 },
290 "internal": {
291 "_baz(uint256)": "50"
292 }
293 },
294 "methodIdentifiers": {
295 "bar(uint256)": "abcd1234"
296 }
297 }
298 }
299 }
300 }
301 });
302
303 let index = build_gas_index(&data);
304 assert_eq!(index.len(), 1);
305
306 let foo = index.get("src/Foo.sol:Foo").unwrap();
307
308 assert_eq!(foo.creation.get("codeDepositCost").unwrap(), "200");
310 assert_eq!(foo.creation.get("executionCost").unwrap(), "infinite");
311
312 assert_eq!(
314 foo.external_by_selector
315 .get(&FuncSelector::new("abcd1234"))
316 .unwrap(),
317 "109"
318 );
319 assert_eq!(
321 foo.external_by_sig
322 .get(&MethodId::new("bar(uint256)"))
323 .unwrap(),
324 "109"
325 );
326
327 assert_eq!(foo.internal.get("_baz(uint256)").unwrap(), "50");
329 }
330
331 #[test]
332 fn test_gas_by_selector() {
333 let data = json!({
334 "contracts": {
335 "src/Foo.sol": {
336 "Foo": {
337 "evm": {
338 "gasEstimates": {
339 "external": { "bar(uint256)": "109" }
340 },
341 "methodIdentifiers": {
342 "bar(uint256)": "abcd1234"
343 }
344 }
345 }
346 }
347 }
348 });
349
350 let index = build_gas_index(&data);
351 let (contract, cost) = gas_by_selector(&index, &FuncSelector::new("abcd1234")).unwrap();
352 assert_eq!(contract, "src/Foo.sol:Foo");
353 assert_eq!(cost, "109");
354 }
355
356 #[test]
357 fn test_gas_by_name() {
358 let data = json!({
359 "contracts": {
360 "src/Foo.sol": {
361 "Foo": {
362 "evm": {
363 "gasEstimates": {
364 "internal": {
365 "_baz(uint256)": "50",
366 "_baz(uint256,address)": "120"
367 }
368 }
369 }
370 }
371 }
372 }
373 });
374
375 let index = build_gas_index(&data);
376 let results = gas_by_name(&index, "_baz");
377 assert_eq!(results.len(), 2);
378 }
379
380 #[test]
381 fn test_gas_for_contract() {
382 let data = json!({
383 "contracts": {
384 "src/Foo.sol": {
385 "Foo": {
386 "evm": {
387 "gasEstimates": {
388 "creation": {
389 "codeDepositCost": "6924600"
390 }
391 }
392 }
393 }
394 }
395 }
396 });
397
398 let index = build_gas_index(&data);
399 let gas = gas_for_contract(&index, "src/Foo.sol", "Foo").unwrap();
400 assert_eq!(gas.creation.get("codeDepositCost").unwrap(), "6924600");
401 }
402
403 #[test]
404 fn test_build_gas_index_from_forge_fixture() {
405 let ast = load_forge_fixture();
406 let index = build_gas_index(&ast);
407 assert!(index.is_empty() || !index.is_empty());
409 }
410
411 #[test]
412 fn test_build_gas_index_from_solc_fixture() {
413 let ast = load_solc_fixture();
414 let index = build_gas_index(&ast);
415
416 assert!(!index.is_empty(), "solc fixture should have gas data");
418
419 let pm_key = index
421 .keys()
422 .find(|k| k.contains("PoolManager.sol:PoolManager"))
423 .expect("should have PoolManager gas data");
424
425 let pm = index.get(pm_key).unwrap();
426
427 assert!(
429 pm.creation.contains_key("codeDepositCost"),
430 "should have codeDepositCost"
431 );
432 assert!(
433 pm.creation.contains_key("executionCost"),
434 "should have executionCost"
435 );
436 assert!(
437 pm.creation.contains_key("totalCost"),
438 "should have totalCost"
439 );
440
441 assert!(
443 !pm.external_by_selector.is_empty(),
444 "should have external function gas estimates"
445 );
446
447 assert!(
449 !pm.internal.is_empty(),
450 "should have internal function gas estimates"
451 );
452 }
453
454 #[test]
455 fn test_gas_by_selector_from_solc_fixture() {
456 let ast = load_solc_fixture();
457 let index = build_gas_index(&ast);
458
459 let result = gas_by_selector(&index, &FuncSelector::new("8da5cb5b"));
461 assert!(result.is_some(), "should find owner() by selector");
462 let (contract, cost) = result.unwrap();
463 assert!(
464 contract.contains("PoolManager"),
465 "should be PoolManager contract"
466 );
467 assert!(!cost.is_empty(), "should have a gas cost");
468 }
469
470 #[test]
471 fn test_gas_by_name_from_solc_fixture() {
472 let ast = load_solc_fixture();
473 let index = build_gas_index(&ast);
474
475 let results = gas_by_name(&index, "_getPool");
477 assert!(!results.is_empty(), "should find _getPool internal gas");
478 }
479
480 #[test]
481 fn test_gas_for_contract_from_solc_fixture() {
482 let ast = load_solc_fixture();
483 let index = build_gas_index(&ast);
484
485 let pm_key = index
487 .keys()
488 .find(|k| k.contains("PoolManager.sol:PoolManager"))
489 .expect("should have PoolManager");
490
491 let parts: Vec<&str> = pm_key.rsplitn(2, ':').collect();
493 let name = parts[0];
494 let path = parts[1];
495
496 let gas = gas_for_contract(&index, path, name);
497 assert!(gas.is_some(), "should find PoolManager contract gas");
498 assert_eq!(
499 gas.unwrap().creation.get("executionCost").unwrap(),
500 "infinite"
501 );
502 }
503}