1use std::fmt::Write as _;
9
10use crate::response::{
11 BreakpointEvent, Level, Output, StateInfo, Stop, StopReason, ValueStatus, VariableInfo,
12};
13
14#[derive(Debug, Clone, Copy, Default)]
16pub struct Renderer {
17 pub color: bool,
19}
20
21impl Renderer {
22 #[must_use]
23 pub const fn new(color: bool) -> Self {
24 Self { color }
25 }
26
27 #[must_use]
30 pub fn render(&self, output: &Output) -> String {
31 let mut text = String::new();
32 for line in self.lines(output) {
33 text.push_str(&line);
34 text.push('\n');
35 }
36 text
37 }
38
39 #[must_use]
41 pub fn lines(&self, output: &Output) -> Vec<String> {
42 match output {
43 Output::Loaded { steps, contracts } => {
44 let mut lines = vec![format!("Loaded trace with {} steps.", self.number(steps))];
45 if contracts.is_empty() {
46 lines.push(self.dim(
47 "No sources matched the executed contracts; stepping is by instruction.",
48 ));
49 } else {
50 let listed = contracts
51 .iter()
52 .map(|contract| match &contract.address {
53 Some(address) => {
54 format!("{} ({})", self.name(&contract.name), self.dim(address))
55 }
56 None => self.name(&contract.name),
57 })
58 .collect::<Vec<_>>();
59 lines.push(format!(
60 "{} {}",
61 self.info("Sources loaded for:"),
62 listed.join(", ")
63 ));
64 }
65 lines
66 }
67 Output::Stop(stop) => self.stop_lines(stop),
68 Output::Message { level, text, note } => {
69 let mut lines = vec![match level {
70 Level::Info => self.info_message(text),
71 Level::Warning => self.warning(text),
72 Level::Note => format!("{} {text}", self.warning("Note:")),
73 }];
74 if let Some(note) = note {
75 lines.push(format!("{} {note}", self.dim("note:")));
76 }
77 lines
78 }
79 Output::Breakpoint { event, id, label } => vec![match event {
80 BreakpointEvent::Set => format!(
81 "{} {label}",
82 self.success(format!("Breakpoint #{id} set at"))
83 ),
84 BreakpointEvent::Cleared => format!(
85 "{} {label}",
86 self.info(format!("Breakpoint #{id} cleared at"))
87 ),
88 }],
89 Output::Breakpoints { breakpoints } => {
90 if breakpoints.is_empty() {
91 vec![self.dim("No breakpoints set.")]
92 } else {
93 breakpoints
94 .iter()
95 .map(|breakpoint| {
96 format!("#{} {}", self.number(breakpoint.id), breakpoint.label)
97 })
98 .collect()
99 }
100 }
101 Output::Backtrace {
102 warning,
103 note,
104 frames,
105 } => {
106 let mut lines = Vec::new();
107 if let Some(warning) = warning {
108 lines.push(self.warning(format!("warning: {warning}")));
109 }
110 if let Some(note) = note {
111 lines.push(format!("{} {note}", self.dim("note:")));
112 }
113 for frame in frames {
114 let mut line = format!("#{:<2} ", frame.index);
115 if let Some(kind) = &frame.kind {
116 let _ = write!(line, "[{kind}] ");
117 }
118 line.push_str(&self.name(&frame.name));
119 if !frame.arguments.is_empty() {
120 let arguments = frame
121 .arguments
122 .iter()
123 .map(|argument| format!("{} = {}", argument.name, argument.value))
124 .collect::<Vec<_>>()
125 .join(", ");
126 let _ = write!(line, "({arguments})");
127 }
128 if let Some(location) = &frame.location {
129 let _ = write!(line, " at {location}");
130 }
131 if let Some(address) = &frame.address {
132 let _ = write!(line, " ({})", self.address(address));
133 }
134 line.push_str(&self.dim(format!(" step {}, PC {}", frame.step, frame.pc)));
135 lines.push(line);
136 }
137 lines
138 }
139 Output::Listing {
140 path,
141 current_line,
142 lines,
143 } => {
144 let mut out = vec![format!("{}:{}", self.info(path), self.number(current_line))];
145 for line in lines {
146 if line.current {
147 out.push(format!(
148 "{} {}",
149 self.bold(format!("=> {:>5} |", line.line)),
150 self.bold(&line.text)
151 ));
152 } else {
153 out.push(format!(
154 "{} {}",
155 self.dim(format!(" {:>5} |", line.line)),
156 line.text
157 ));
158 }
159 }
160 out
161 }
162 Output::Stack { words, shown } => {
163 if words.is_empty() {
164 return vec![self.dim("Stack: empty")];
165 }
166 let mut lines = vec![format!(
167 "{} {} words, top first",
168 self.info("Stack:"),
169 self.number(words.len())
170 )];
171 for (index, word) in words.iter().enumerate().rev().take(*shown) {
172 lines.push(format!(" [{index}] {}", self.number(word)));
173 }
174 if *shown < words.len() {
175 lines.push(self.dim(format!(" ... {} more below", words.len() - shown)));
176 }
177 lines
178 }
179 Output::Memory(memory) => {
180 let range = if memory.start > 0 || memory.end < memory.total {
181 format!(", showing bytes {}..{}", memory.start, memory.end)
182 } else {
183 String::new()
184 };
185 let mut lines = vec![format!(
186 "{} {} bytes{range}",
187 self.info("Memory:"),
188 self.number(memory.total)
189 )];
190 for word in &memory.words {
191 lines.push(format!(
192 "{} {}",
193 self.dim(format!("0x{:04x}:", word.offset)),
194 word.hex
195 ));
196 }
197 lines
198 }
199 Output::Storage { address, slots } => {
200 if slots.is_empty() {
201 return vec![self.dim("Storage: no slots read or written yet.")];
202 }
203 let mut lines = vec![match address {
204 Some(address) => {
205 format!(
206 "{} {}",
207 self.info("Storage:"),
208 self.dim(format!("of {address}"))
209 )
210 }
211 None => self.info("Storage:"),
212 }];
213 for slot in slots {
214 let was = slot
215 .was
216 .as_ref()
217 .map(|was| self.dim(format!(" (was {was})")))
218 .unwrap_or_default();
219 lines.push(format!(" {} = {}{was}", slot.slot, slot.value));
220 }
221 lines
222 }
223 Output::TransientStorage { address, slots } => {
224 if slots.is_empty() {
225 return vec![self.dim(
226 "Transient storage: nothing read or written yet; every slot is zero.",
227 )];
228 }
229 let mut lines = vec![match address {
230 Some(address) => format!(
231 "{} {}",
232 self.info("Transient storage:"),
233 self.dim(format!("of {address}"))
234 ),
235 None => self.info("Transient storage:"),
236 }];
237 for slot in slots {
238 lines.push(format!(" {} = {}", slot.slot, slot.value));
239 }
240 lines
241 }
242 Output::Calldata { bytes, data } => vec![
243 format!("{} {} bytes", self.info("Calldata:"), self.number(bytes)),
244 data.clone(),
245 ],
246 Output::ReturnData { bytes: 0, .. } => vec![self.dim("Return data: empty.")],
247 Output::ReturnData { bytes, data } => vec![
248 format!("{} {} bytes", self.info("Return data:"), self.number(bytes)),
249 data.clone(),
250 ],
251 Output::Variables {
252 warning,
253 pc,
254 locals,
255 unavailable,
256 state,
257 transient,
258 } => {
259 let mut lines = Vec::new();
260 if let Some(warning) = warning {
261 lines.push(self.warning(format!("warning: {warning}")));
262 }
263 if locals.is_empty() {
264 lines.push(match unavailable {
267 Some(reason) => format!(
268 "{} locals are unavailable here: {reason}",
269 self.dim("Variables:")
270 ),
271 None => format!(
272 "{} no variables in scope at PC {}",
273 self.dim("Variables:"),
274 self.number(pc)
275 ),
276 });
277 }
278 for variable in locals {
279 lines.push(self.variable(variable));
280 }
281 match state {
282 StateInfo::Variables { variables } => {
283 lines.push(self.dim("State:"));
284 for variable in variables {
285 lines.push(self.variable(variable));
286 }
287 }
288 StateInfo::None if !transient.is_empty() => lines.push(self.dim("State:")),
290 StateInfo::None => {
291 lines.push(self.dim("State: this contract declares no state variables"));
292 }
293 StateInfo::NoLayout => lines.push(self.dim(
294 "State: no storage layout loaded; compile with `--storage-layout` to \
295 read state variables",
296 )),
297 StateInfo::NoStorage => {
298 lines.push(self.dim("State: no storage was recorded for this step"));
299 }
300 }
301 for variable in transient {
302 lines.push(self.variable(variable));
303 }
304 lines
305 }
306 Output::Variable { warning, variable } => {
307 let mut lines = Vec::new();
308 if let Some(warning) = warning {
309 lines.push(self.warning(format!("warning: {warning}")));
310 }
311 lines.push(self.variable(variable));
312 lines
313 }
314 Output::Resources { contracts, json } => {
315 if *json {
316 let document = serde_json::json!({ "contracts": contracts });
317 return serde_json::to_string_pretty(&document)
318 .unwrap_or_default()
319 .lines()
320 .map(str::to_owned)
321 .collect();
322 }
323 if contracts.is_empty() {
324 return vec![self.warning("No contract sources are loaded.")];
325 }
326 let mut lines = Vec::new();
327 for contract in contracts {
328 lines.push(format!(
329 "{} {} {}",
330 self.name(&contract.name),
331 self.dim(contract.address.as_deref().unwrap_or("<any address>")),
332 self.dim(format!("[{}]", contract.environment))
333 ));
334 for path in &contract.sources {
335 lines.push(format!(" {path}"));
336 }
337 }
338 lines
339 }
340 Output::Help { lines } => lines.clone(),
341 Output::Tui | Output::Quit => Vec::new(),
342 }
343 }
344
345 fn stop_lines(&self, stop: &Stop) -> Vec<String> {
348 let mut lines = Vec::new();
349 match &stop.reason {
350 StopReason::Initial | StopReason::Moved => {}
351 StopReason::Breakpoint { id, label } => lines.push(format!(
352 "{} {}, {label}",
353 self.success(format!("Breakpoint #{id} hit at step")),
354 self.number(stop.step)
355 )),
356 StopReason::End => lines.push(format!(
357 "{} {}",
358 self.info("End of trace at step"),
359 self.number(stop.step)
360 )),
361 StopReason::Start => lines.push(format!(
362 "{} {}",
363 self.info("Start of trace at step"),
364 self.number(stop.step)
365 )),
366 }
367 let status = format!(
368 "step {}/{}, pc {}, {}, gas {}",
369 self.number(stop.step),
370 self.number(stop.last_step),
371 self.number(stop.pc),
372 self.opcode(&stop.op),
373 self.number(stop.gas)
374 );
375 match &stop.location {
376 Some(location) => {
377 let function = location
378 .function
379 .as_deref()
380 .map(|name| format!(" in {}", self.name(name)))
381 .unwrap_or_default();
382 let generated = if location.generated {
383 self.dim(" (compiler-generated code for this line)")
384 } else {
385 String::new()
386 };
387 lines.push(format!(
388 "{}:{}{function}{generated} {}",
389 self.info(&location.path),
390 self.number(location.line),
391 self.dim(format!("({status})"))
392 ));
393 if stop.stack.is_none() {
394 if let Some(text) = &location.text {
395 lines.push(format!(
396 "{} {text}",
397 self.dim(format!("{:>5} |", location.line))
398 ));
399 }
400 }
401 }
402 None if stop.has_source => {
403 let address = stop
404 .address
405 .as_deref()
406 .map(|address| format!(" in {}", self.address(address)))
407 .unwrap_or_default();
408 lines.push(format!(
409 "{} {}",
410 self.dim(format!("no source for this step{address}")),
411 self.dim(format!("({status})"))
412 ));
413 }
414 None => lines.push(status),
415 }
416 if let Some(stack) = &stop.stack {
417 lines.push(format!(
418 "{} {}",
419 self.info("Stack:"),
420 self.stack_summary(stack)
421 ));
422 }
423 if let Some(note) = &stop.note {
424 lines.push(format!("{} {note}", self.warning("Note:")));
425 }
426 lines
427 }
428
429 fn stack_summary(&self, top_first: &[String]) -> String {
431 if top_first.is_empty() {
432 return self.dim("empty");
433 }
434 let total = top_first.len();
435 let mut parts = top_first
436 .iter()
437 .take(4)
438 .enumerate()
439 .map(|(offset, word)| {
440 format!(
441 "{} {}",
442 self.dim(format!("[{}]", total - 1 - offset)),
443 self.number(shorten_hex(word))
444 )
445 })
446 .collect::<Vec<_>>();
447 if total > 4 {
448 parts.push(self.dim(format!("... +{} more", total - 4)));
449 }
450 parts.join(" ")
451 }
452
453 fn variable(&self, variable: &VariableInfo) -> String {
454 let value = match variable.status {
455 ValueStatus::Unavailable => self.warning(&variable.value),
456 _ => self.success(&variable.value),
457 };
458 let place = variable
459 .place
460 .as_ref()
461 .map(|place| self.dim(format!(" [{place}]")))
462 .unwrap_or_default();
463 format!(
464 "{} {} = {value}{place}",
465 self.info(&variable.ty),
466 self.bold(&variable.name)
467 )
468 }
469
470 fn info_message(&self, text: &str) -> String {
471 match text.split_once(": ") {
473 Some((label, rest)) if !label.contains(' ') || label.starts_with("Mode") => {
474 format!("{} {rest}", self.info(format!("{label}:")))
475 }
476 _ => self.info(text),
477 }
478 }
479
480 fn paint(&self, text: impl std::fmt::Display, code: &str) -> String {
481 if self.color {
482 format!("\x1b[{code}m{text}\x1b[0m")
483 } else {
484 text.to_string()
485 }
486 }
487
488 fn bold(&self, text: impl std::fmt::Display) -> String {
489 self.paint(text, "1")
490 }
491
492 fn dim(&self, text: impl std::fmt::Display) -> String {
493 self.paint(text, "2")
494 }
495
496 fn info(&self, text: impl std::fmt::Display) -> String {
497 self.paint(text, "96")
498 }
499
500 fn success(&self, text: impl std::fmt::Display) -> String {
501 self.paint(text, "92")
502 }
503
504 fn warning(&self, text: impl std::fmt::Display) -> String {
505 self.paint(text, "93")
506 }
507
508 fn number(&self, text: impl std::fmt::Display) -> String {
509 self.paint(text, "93")
510 }
511
512 fn opcode(&self, text: impl std::fmt::Display) -> String {
513 self.paint(text, "94")
514 }
515
516 fn name(&self, text: impl std::fmt::Display) -> String {
517 self.paint(text, "95")
518 }
519
520 fn address(&self, text: impl std::fmt::Display) -> String {
521 self.paint(text, "95")
522 }
523}
524
525#[must_use]
527pub fn shorten_hex(value: &str) -> String {
528 let Some(digits) = value.strip_prefix("0x") else {
529 return value.to_owned();
530 };
531 if value.len() <= 10 {
532 return value.to_owned();
533 }
534 let head = digits.chars().take(4).collect::<String>();
535 format!("0x{head}...")
536}
537
538#[cfg(test)]
539mod tests {
540 use super::{shorten_hex, Renderer};
541 use crate::response::{
542 Level, Output, SlotInfo, StateInfo, Stop, StopLocation, StopReason, ValueStatus,
543 VariableInfo,
544 };
545
546 #[test]
547 fn transient_storage_lists_its_slots_or_says_every_one_is_zero() {
548 let renderer = Renderer::new(false);
549 assert_eq!(
550 renderer.render(&Output::TransientStorage {
551 address: Some("0xabc".to_owned()),
552 slots: vec![SlotInfo {
553 slot: "0x0".to_owned(),
554 value: "0x2".to_owned(),
555 was: None,
556 }],
557 }),
558 "Transient storage: of 0xabc\n 0x0 = 0x2\n"
559 );
560 assert_eq!(
561 renderer.render(&Output::TransientStorage {
562 address: None,
563 slots: Vec::new(),
564 }),
565 "Transient storage: nothing read or written yet; every slot is zero.\n"
566 );
567 }
568
569 #[test]
570 fn return_data_is_its_size_and_hex_or_says_it_is_empty() {
571 let renderer = Renderer::new(false);
572 assert_eq!(
573 renderer.render(&Output::ReturnData {
574 bytes: 1,
575 data: "0x2a".to_owned()
576 }),
577 "Return data: 1 bytes\n0x2a\n"
578 );
579 assert_eq!(
580 renderer.render(&Output::ReturnData {
581 bytes: 0,
582 data: "0x".to_owned()
583 }),
584 "Return data: empty.\n"
585 );
586 }
587
588 #[test]
589 fn transient_variables_follow_the_state_and_keep_its_reason() {
590 let renderer = Renderer::new(false);
591 let variables = |state| Output::Variables {
592 warning: None,
593 pc: 7,
594 locals: vec![VariableInfo {
595 name: "a".to_owned(),
596 ty: "uint256".to_owned(),
597 value: "1".to_owned(),
598 raw: None,
599 status: ValueStatus::Decoded,
600 place: Some("stack+1".to_owned()),
601 }],
602 unavailable: None,
603 state,
604 transient: vec![VariableInfo {
605 name: "locked".to_owned(),
606 ty: "bool".to_owned(),
607 value: "true".to_owned(),
608 raw: None,
609 status: ValueStatus::Decoded,
610 place: Some("transient slot 0x0".to_owned()),
611 }],
612 };
613 assert_eq!(
615 renderer.render(&variables(StateInfo::None)),
616 "uint256 a = 1 [stack+1]\nState:\nbool locked = true [transient slot 0x0]\n"
617 );
618 assert!(renderer
620 .render(&variables(StateInfo::NoLayout))
621 .contains("no storage layout loaded"));
622 }
623
624 #[test]
625 fn a_stop_is_one_location_line_and_the_source_line() {
626 let renderer = Renderer::new(false);
627 let stop = Stop {
628 reason: StopReason::Breakpoint {
629 id: 1,
630 label: "Counter.sol:7".to_owned(),
631 },
632 step: 12,
633 last_step: 99,
634 pc: 34,
635 op: "PUSH1".to_owned(),
636 gas: 5000,
637 location: Some(StopLocation {
638 path: "Counter.sol".to_owned(),
639 line: 7,
640 function: Some("increment".to_owned()),
641 generated: false,
642 text: Some(" count += 1;".to_owned()),
643 }),
644 address: None,
645 has_source: true,
646 stack: None,
647 note: Some("something".to_owned()),
648 };
649 assert_eq!(
650 renderer.render(&Output::Stop(stop)),
651 "Breakpoint #1 hit at step 12, Counter.sol:7\n\
652 Counter.sol:7 in increment (step 12/99, pc 34, PUSH1, gas 5000)\n \
653 7 | count += 1;\n\
654 Note: something\n"
655 );
656 let bare = Stop {
657 reason: StopReason::Moved,
658 step: 0,
659 last_step: 3,
660 pc: 0,
661 op: "PUSH1".to_owned(),
662 gas: 1,
663 location: None,
664 address: Some("0xabc".to_owned()),
665 has_source: false,
666 stack: Some(vec!["0x2".to_owned(), "0x1".to_owned()]),
667 note: None,
668 };
669 assert_eq!(
670 renderer.render(&Output::Stop(bare)),
671 "step 0/3, pc 0, PUSH1, gas 1\nStack: [1] 0x2 [0] 0x1\n"
672 );
673 }
674
675 #[test]
676 fn messages_and_hex_are_plain_without_color() {
677 let renderer = Renderer::new(false);
678 assert_eq!(
679 renderer.render(&Output::Message {
680 level: Level::Warning,
681 text: "Unknown command: x".to_owned(),
682 note: Some("try help".to_owned()),
683 }),
684 "Unknown command: x\nnote: try help\n"
685 );
686 assert_eq!(shorten_hex("0x1234"), "0x1234");
687 assert_eq!(shorten_hex("0x123456789abcdef0"), "0x1234...");
688 assert_eq!(shorten_hex("plain"), "plain");
689 assert!(Renderer::new(true)
690 .render(&Output::Help {
691 lines: vec!["x".to_owned()]
692 })
693 .starts_with('x'));
694 }
695}