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 mod hh;
|
||||||
pub use artifact_output::*;
|
pub use artifact_output::*;
|
||||||
|
|
||||||
mod resolver;
|
pub mod resolver;
|
||||||
pub use hh::{HardhatArtifact, HardhatArtifacts};
|
pub use hh::{HardhatArtifact, HardhatArtifacts};
|
||||||
pub use resolver::Graph;
|
pub use resolver::Graph;
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet, VecDeque},
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
|
fmt, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,6 +59,9 @@ use solang_parser::pt::{Import, Loc, SourceUnitPart};
|
||||||
|
|
||||||
use crate::{error::Result, utils, ProjectPathsConfig, Solc, SolcError, Source, Sources};
|
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.
|
/// 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`
|
/// 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 {
|
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.
|
/// Returns a list of nodes the given node index points to for the given kind.
|
||||||
pub fn imported_nodes(&self, from: usize) -> &[usize] {
|
pub fn imported_nodes(&self, from: usize) -> &[usize] {
|
||||||
self.edges.imported_nodes(from)
|
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
|
/// Returns all the resolved files and their index in the graph
|
||||||
pub fn files(&self) -> &HashMap<PathBuf, usize> {
|
pub fn files(&self) -> &HashMap<PathBuf, usize> {
|
||||||
&self.edges.indices
|
&self.edges.indices
|
||||||
|
@ -148,6 +169,9 @@ impl Graph {
|
||||||
&self.nodes[index]
|
&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
|
/// Returns an iterator that yields all nodes of the dependency tree that the given node id
|
||||||
/// spans, starting with the node itself.
|
/// 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)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
struct SolData {
|
struct SolData {
|
||||||
|
@ -850,4 +891,47 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);
|
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