From 7d2d96d7615a70504c9893cd69166dcb732904d9 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 19 Feb 2022 14:55:21 +0100 Subject: [PATCH] feat(solc): add tree printer implementation (#933) * feat(solc): add tree printer implementation * test: feature gate windows * typos --- ethers-solc/src/lib.rs | 2 +- .../src/{resolver.rs => resolver/mod.rs} | 84 ++++++++ ethers-solc/src/resolver/tree.rs | 183 ++++++++++++++++++ 3 files changed, 268 insertions(+), 1 deletion(-) rename ethers-solc/src/{resolver.rs => resolver/mod.rs} (92%) create mode 100644 ethers-solc/src/resolver/tree.rs diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index a74d455a..4e99605a 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -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; diff --git a/ethers-solc/src/resolver.rs b/ethers-solc/src/resolver/mod.rs similarity index 92% rename from ethers-solc/src/resolver.rs rename to ethers-solc/src/resolver/mod.rs index 16bfbc34..c8e1ca60 100644 --- a/ethers-solc/src/resolver.rs +++ b/ethers-solc/src/resolver/mod.rs @@ -48,6 +48,7 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, + fmt, io, path::{Path, PathBuf}, }; @@ -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` @@ -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 { &self.edges.indices @@ -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. /// @@ -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 { @@ -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::::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::::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 + ); + } } diff --git a/ethers-solc/src/resolver/tree.rs b/ethers-solc/src/resolver/tree.rs new file mode 100644 index 00000000..230036b6 --- /dev/null +++ b/ethers-solc/src/resolver/tree.rs @@ -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 { + 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, + levels_continue: &mut Vec, + write_stack: &mut Vec, + 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, + levels_continue: &mut Vec, + write_stack: &mut Vec, + 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(()) +}