feat(solc): add tree printer implementation (#933)

* feat(solc): add tree printer implementation

* test: feature gate windows

* typos
This commit is contained in:
Matthias Seitz 2022-02-19 14:55:21 +01:00 committed by GitHub
parent 5b2c1fa6f8
commit 7d2d96d761
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 268 additions and 1 deletions

View File

@ -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;

View File

@ -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
);
}
} }

View File

@ -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(())
}