Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat(solc): add tree printer implementation #933

Merged
merged 3 commits into from
Feb 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ethers-solc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub mod cache;
pub mod hh;
pub use artifact_output::*;

mod resolver;
pub mod resolver;
pub use hh::{HardhatArtifact, HardhatArtifacts};
pub use resolver::Graph;

Expand Down
84 changes: 84 additions & 0 deletions ethers-solc/src/resolver.rs → ethers-solc/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

use std::{
collections::{HashMap, HashSet, VecDeque},
fmt, io,
path::{Path, PathBuf},
};

Expand All @@ -58,6 +59,9 @@ use solang_parser::pt::{Import, Loc, SourceUnitPart};

use crate::{error::Result, utils, ProjectPathsConfig, Solc, SolcError, Source, Sources};

mod tree;
pub use tree::{print, Charset, TreeOptions};

/// The underlying edges of the graph which only contains the raw relationship data.
///
/// This is kept separate from the `Graph` as the `Node`s get consumed when the `Solc` to `Sources`
Expand Down Expand Up @@ -129,11 +133,28 @@ pub struct Graph {
}

impl Graph {
/// Print the graph to `StdOut`
pub fn print(&self) {
self.print_with_options(Default::default())
}

/// Print the graph to `StdOut` using the provided `TreeOptions`
pub fn print_with_options(&self, opts: TreeOptions) {
let stdout = io::stdout();
let mut out = stdout.lock();
tree::print(self, &opts, &mut out).expect("failed to write to stdout.")
}

/// Returns a list of nodes the given node index points to for the given kind.
pub fn imported_nodes(&self, from: usize) -> &[usize] {
self.edges.imported_nodes(from)
}

/// Returns `true` if the given node has any outgoing edges.
pub(crate) fn has_outgoing_edges(&self, index: usize) -> bool {
!self.edges.edges[index].is_empty()
}

/// Returns all the resolved files and their index in the graph
pub fn files(&self) -> &HashMap<PathBuf, usize> {
&self.edges.indices
Expand All @@ -148,6 +169,9 @@ impl Graph {
&self.nodes[index]
}

pub(crate) fn display_node(&self, index: usize) -> DisplayNode {
DisplayNode { node: self.node(index), root: &self.root }
}
/// Returns an iterator that yields all nodes of the dependency tree that the given node id
/// spans, starting with the node itself.
///
Expand Down Expand Up @@ -646,6 +670,23 @@ impl Node {
}
}

/// Helper type for formatting a node
pub(crate) struct DisplayNode<'a> {
node: &'a Node,
root: &'a PathBuf,
}

impl<'a> fmt::Display for DisplayNode<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path = utils::source_name(&self.node.path, self.root);
write!(f, "{}", path.display())?;
if let Some(ref v) = self.node.data.version {
write!(f, " {}", v.data())?;
}
Ok(())
}
}

#[derive(Debug, Clone)]
#[allow(unused)]
struct SolData {
Expand Down Expand Up @@ -850,4 +891,47 @@ mod tests {
);
assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);
}

#[test]
#[cfg(not(target_os = "windows"))]
fn can_print_dapp_sample_graph() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
let paths = ProjectPathsConfig::dapptools(root).unwrap();
let graph = Graph::resolve(&paths).unwrap();
let mut out = Vec::<u8>::new();
tree::print(&graph, &Default::default(), &mut out).unwrap();

assert_eq!(
"
src/Dapp.sol >=0.6.6
src/Dapp.t.sol >=0.6.6
├── lib/ds-test/src/test.sol >=0.4.23
└── src/Dapp.sol >=0.6.6
"
.trim_start()
.as_bytes()
.to_vec(),
out
);
}

#[test]
#[cfg(not(target_os = "windows"))]
fn can_print_hardhat_sample_graph() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");
let paths = ProjectPathsConfig::hardhat(root).unwrap();
let graph = Graph::resolve(&paths).unwrap();
let mut out = Vec::<u8>::new();
tree::print(&graph, &Default::default(), &mut out).unwrap();
assert_eq!(
"
contracts/Greeter.sol >=0.6.0
└── node_modules/hardhat/console.sol >= 0.4.22 <0.9.0
"
.trim_start()
.as_bytes()
.to_vec(),
out
);
}
}
183 changes: 183 additions & 0 deletions ethers-solc/src/resolver/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use crate::Graph;
use std::{collections::HashSet, io, io::Write, str::FromStr};

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Charset {
Utf8,
Ascii,
}

impl Default for Charset {
fn default() -> Self {
// when operating in a console on windows non-UTF-8 byte sequences are not supported on
// stdout, See also [`StdoutLock`]
#[cfg(target_os = "windows")]
{
Charset::Ascii
}
#[cfg(not(target_os = "windows"))]
Charset::Utf8
}
}

impl FromStr for Charset {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"utf8" => Ok(Charset::Utf8),
"ascii" => Ok(Charset::Ascii),
s => Err(format!("invalid charset: {}", s)),
}
}
}

/// Options to configure formatting
#[derive(Debug, Clone, Default)]
pub struct TreeOptions {
/// The style of characters to use.
pub charset: Charset,
/// If `true`, duplicate imports will be repeated.
/// If `false`, duplicates are suffixed with `(*)`, and their imports
/// won't be shown.
pub no_dedupe: bool,
}

/// Internal helper type for symbols
struct Symbols {
down: &'static str,
tee: &'static str,
ell: &'static str,
right: &'static str,
}

static UTF8_SYMBOLS: Symbols = Symbols { down: "│", tee: "├", ell: "└", right: "─" };

static ASCII_SYMBOLS: Symbols = Symbols { down: "|", tee: "|", ell: "`", right: "-" };

pub fn print(graph: &Graph, opts: &TreeOptions, out: &mut dyn Write) -> io::Result<()> {
let symbols = match opts.charset {
Charset::Utf8 => &UTF8_SYMBOLS,
Charset::Ascii => &ASCII_SYMBOLS,
};

// used to determine whether to display `(*)`
let mut visited_imports = HashSet::new();

// A stack of bools used to determine where | symbols should appear
// when printing a line.
let mut levels_continue = Vec::new();
// used to detect dependency cycles when --no-dedupe is used.
// contains a `Node` for each level.
let mut write_stack = Vec::new();

for (node_index, _) in graph.input_nodes().enumerate() {
print_node(
graph,
node_index,
symbols,
opts.no_dedupe,
&mut visited_imports,
&mut levels_continue,
&mut write_stack,
out,
)?;
}

Ok(())
}

#[allow(clippy::too_many_arguments)]
fn print_node(
graph: &Graph,
node_index: usize,
symbols: &Symbols,
no_dedupe: bool,
visited_imports: &mut HashSet<usize>,
levels_continue: &mut Vec<bool>,
write_stack: &mut Vec<usize>,
out: &mut dyn Write,
) -> io::Result<()> {
let new_node = no_dedupe || visited_imports.insert(node_index);

if let Some((last_continues, rest)) = levels_continue.split_last() {
for continues in rest {
let c = if *continues { symbols.down } else { " " };
write!(out, "{} ", c)?;
}

let c = if *last_continues { symbols.tee } else { symbols.ell };
write!(out, "{0}{1}{1} ", c, symbols.right)?;
}

let in_cycle = write_stack.contains(&node_index);
// if this node does not have any outgoing edges, don't include the (*)
// since there isn't really anything "deduplicated", and it generally just
// adds noise.
let has_deps = graph.has_outgoing_edges(node_index);
let star = if (new_node && !in_cycle) || !has_deps { "" } else { " (*)" };

writeln!(out, "{}{}", graph.display_node(node_index), star)?;

if !new_node || in_cycle {
return Ok(())
}
write_stack.push(node_index);

print_imports(
graph,
node_index,
symbols,
no_dedupe,
visited_imports,
levels_continue,
write_stack,
out,
)?;

write_stack.pop();

Ok(())
}

/// Prints all the imports of a node
#[allow(clippy::too_many_arguments, clippy::ptr_arg)]
fn print_imports(
graph: &Graph,
node_index: usize,
symbols: &Symbols,
no_dedupe: bool,
visited_imports: &mut HashSet<usize>,
levels_continue: &mut Vec<bool>,
write_stack: &mut Vec<usize>,
out: &mut dyn Write,
) -> io::Result<()> {
let imports = graph.imported_nodes(node_index);
if imports.is_empty() {
return Ok(())
}

for continues in &**levels_continue {
let c = if *continues { symbols.down } else { " " };
write!(out, "{} ", c)?;
}

let mut iter = imports.iter().peekable();

while let Some(import) = iter.next() {
levels_continue.push(iter.peek().is_some());
print_node(
graph,
*import,
symbols,
no_dedupe,
visited_imports,
levels_continue,
write_stack,
out,
)?;
levels_continue.pop();
}

Ok(())
}