feat(solc): add tree printer implementation (#933)
* feat(solc): add tree printer implementation * test: feature gate windows * typos
This commit is contained in:
parent
5b2c1fa6f8
commit
7d2d96d761
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<PathBuf, usize> {
|
||||
&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::<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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
Loading…
Reference in New Issue