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

Commit

Permalink
feat(solc): add tree printer implementation (#933)
Browse files Browse the repository at this point in the history
* feat(solc): add tree printer implementation

* test: feature gate windows

* typos
  • Loading branch information
mattsse authored Feb 19, 2022
1 parent 5b2c1fa commit 7d2d96d
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 1 deletion.
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(())
}

0 comments on commit 7d2d96d

Please sign in to comment.