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