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_typed(
165 decl: &crate::solc_ast::DeclNode,
166 index: &GasIndex,
167 decl_index: &HashMap<i64, crate::solc_ast::DeclNode>,
168 node_id_to_source_path: &HashMap<i64, String>,
169) -> Option<String> {
170 use crate::solc_ast::DeclNode;
171
172 let (contract_name, source_unit_scope) = match decl {
174 DeclNode::ContractDefinition(c) => (c.name.as_str(), c.scope?),
175 _ => {
176 let scope_id = decl.scope()?;
178 let scope_decl = decl_index.get(&scope_id)?;
179 let contract = match scope_decl {
180 DeclNode::ContractDefinition(c) => c,
181 _ => return None,
182 };
183 (contract.name.as_str(), contract.scope?)
184 }
185 };
186
187 let abs_path = node_id_to_source_path.get(&source_unit_scope)?;
189
190 let exact_key = format!("{abs_path}:{contract_name}");
192 if index.contains_key(&exact_key) {
193 return Some(exact_key);
194 }
195
196 let file_name = std::path::Path::new(abs_path.as_str())
198 .file_name()?
199 .to_str()?;
200 let suffix = format!("{file_name}:{contract_name}");
201 index.keys().find(|k| k.ends_with(&suffix)).cloned()
202}
203
204pub fn format_gas(cost: &str) -> String {
208 if cost == "infinite" {
209 return "infinite".to_string();
210 }
211 if let Ok(n) = cost.parse::<u64>() {
213 let s = n.to_string();
214 let mut result = String::new();
215 for (i, c) in s.chars().rev().enumerate() {
216 if i > 0 && i % 3 == 0 {
217 result.push(',');
218 }
219 result.push(c);
220 }
221 result.chars().rev().collect()
222 } else {
223 cost.to_string()
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use serde_json::json;
231
232 fn load_solc_fixture() -> Value {
234 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
235 let raw: Value = serde_json::from_str(&data).expect("valid json");
236 crate::solc::normalize_solc_output(raw, None)
237 }
238
239 #[test]
240 fn test_format_gas_number() {
241 assert_eq!(format_gas("109"), "109");
242 assert_eq!(format_gas("2595"), "2,595");
243 assert_eq!(format_gas("6924600"), "6,924,600");
244 assert_eq!(format_gas("28088"), "28,088");
245 }
246
247 #[test]
248 fn test_format_gas_infinite() {
249 assert_eq!(format_gas("infinite"), "infinite");
250 }
251
252 #[test]
253 fn test_format_gas_unknown() {
254 assert_eq!(format_gas("unknown"), "unknown");
255 }
256
257 #[test]
258 fn test_build_gas_index_empty() {
259 let data = json!({});
260 let index = build_gas_index(&data);
261 assert!(index.is_empty());
262 }
263
264 #[test]
265 fn test_build_gas_index_no_contracts() {
266 let data = json!({ "sources": {}, "contracts": {} });
267 let index = build_gas_index(&data);
268 assert!(index.is_empty());
269 }
270
271 #[test]
272 fn test_build_gas_index_basic() {
273 let data = json!({
274 "contracts": {
275 "src/Foo.sol": {
276 "Foo": {
277 "evm": {
278 "gasEstimates": {
279 "creation": {
280 "codeDepositCost": "200",
281 "executionCost": "infinite",
282 "totalCost": "infinite"
283 },
284 "external": {
285 "bar(uint256)": "109"
286 },
287 "internal": {
288 "_baz(uint256)": "50"
289 }
290 },
291 "methodIdentifiers": {
292 "bar(uint256)": "abcd1234"
293 }
294 }
295 }
296 }
297 }
298 });
299
300 let index = build_gas_index(&data);
301 assert_eq!(index.len(), 1);
302
303 let foo = index.get("src/Foo.sol:Foo").unwrap();
304
305 assert_eq!(foo.creation.get("codeDepositCost").unwrap(), "200");
307 assert_eq!(foo.creation.get("executionCost").unwrap(), "infinite");
308
309 assert_eq!(
311 foo.external_by_selector
312 .get(&FuncSelector::new("abcd1234"))
313 .unwrap(),
314 "109"
315 );
316 assert_eq!(
318 foo.external_by_sig
319 .get(&MethodId::new("bar(uint256)"))
320 .unwrap(),
321 "109"
322 );
323
324 assert_eq!(foo.internal.get("_baz(uint256)").unwrap(), "50");
326 }
327
328 #[test]
329 fn test_gas_by_selector() {
330 let data = json!({
331 "contracts": {
332 "src/Foo.sol": {
333 "Foo": {
334 "evm": {
335 "gasEstimates": {
336 "external": { "bar(uint256)": "109" }
337 },
338 "methodIdentifiers": {
339 "bar(uint256)": "abcd1234"
340 }
341 }
342 }
343 }
344 }
345 });
346
347 let index = build_gas_index(&data);
348 let (contract, cost) = gas_by_selector(&index, &FuncSelector::new("abcd1234")).unwrap();
349 assert_eq!(contract, "src/Foo.sol:Foo");
350 assert_eq!(cost, "109");
351 }
352
353 #[test]
354 fn test_gas_by_name() {
355 let data = json!({
356 "contracts": {
357 "src/Foo.sol": {
358 "Foo": {
359 "evm": {
360 "gasEstimates": {
361 "internal": {
362 "_baz(uint256)": "50",
363 "_baz(uint256,address)": "120"
364 }
365 }
366 }
367 }
368 }
369 }
370 });
371
372 let index = build_gas_index(&data);
373 let results = gas_by_name(&index, "_baz");
374 assert_eq!(results.len(), 2);
375 }
376
377 #[test]
378 fn test_gas_for_contract() {
379 let data = json!({
380 "contracts": {
381 "src/Foo.sol": {
382 "Foo": {
383 "evm": {
384 "gasEstimates": {
385 "creation": {
386 "codeDepositCost": "6924600"
387 }
388 }
389 }
390 }
391 }
392 }
393 });
394
395 let index = build_gas_index(&data);
396 let gas = gas_for_contract(&index, "src/Foo.sol", "Foo").unwrap();
397 assert_eq!(gas.creation.get("codeDepositCost").unwrap(), "6924600");
398 }
399
400 #[test]
401 fn test_build_gas_index_from_solc_fixture() {
402 let ast = load_solc_fixture();
403 let index = build_gas_index(&ast);
404
405 assert!(!index.is_empty(), "solc fixture should have gas data");
407
408 let pm_key = index
410 .keys()
411 .find(|k| k.contains("PoolManager.sol:PoolManager"))
412 .expect("should have PoolManager gas data");
413
414 let pm = index.get(pm_key).unwrap();
415
416 assert!(
418 pm.creation.contains_key("codeDepositCost"),
419 "should have codeDepositCost"
420 );
421 assert!(
422 pm.creation.contains_key("executionCost"),
423 "should have executionCost"
424 );
425 assert!(
426 pm.creation.contains_key("totalCost"),
427 "should have totalCost"
428 );
429
430 assert!(
432 !pm.external_by_selector.is_empty(),
433 "should have external function gas estimates"
434 );
435
436 assert!(
438 !pm.internal.is_empty(),
439 "should have internal function gas estimates"
440 );
441 }
442
443 #[test]
444 fn test_gas_by_selector_from_solc_fixture() {
445 let ast = load_solc_fixture();
446 let index = build_gas_index(&ast);
447
448 let result = gas_by_selector(&index, &FuncSelector::new("8da5cb5b"));
450 assert!(result.is_some(), "should find owner() by selector");
451 let (contract, cost) = result.unwrap();
452 assert!(
453 contract.contains("PoolManager"),
454 "should be PoolManager contract"
455 );
456 assert!(!cost.is_empty(), "should have a gas cost");
457 }
458
459 #[test]
460 fn test_gas_by_name_from_solc_fixture() {
461 let ast = load_solc_fixture();
462 let index = build_gas_index(&ast);
463
464 let results = gas_by_name(&index, "_getPool");
466 assert!(!results.is_empty(), "should find _getPool internal gas");
467 }
468
469 #[test]
470 fn test_gas_for_contract_from_solc_fixture() {
471 let ast = load_solc_fixture();
472 let index = build_gas_index(&ast);
473
474 let pm_key = index
476 .keys()
477 .find(|k| k.contains("PoolManager.sol:PoolManager"))
478 .expect("should have PoolManager");
479
480 let parts: Vec<&str> = pm_key.rsplitn(2, ':').collect();
482 let name = parts[0];
483 let path = parts[1];
484
485 let gas = gas_for_contract(&index, path, name);
486 assert!(gas.is_some(), "should find PoolManager contract gas");
487 assert_eq!(
488 gas.unwrap().creation.get("executionCost").unwrap(),
489 "infinite"
490 );
491 }
492}