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