foundry_compilers/resolver/
tree.rs

1use crate::{compilers::ParsedSource, Graph};
2use std::{collections::HashSet, io, io::Write, str::FromStr};
3
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
5pub enum Charset {
6    // when operating in a console on windows non-UTF-8 byte sequences are not supported on
7    // stdout, See also [`StdoutLock`]
8    #[cfg_attr(not(target_os = "windows"), default)]
9    Utf8,
10    #[cfg_attr(target_os = "windows", default)]
11    Ascii,
12}
13
14impl FromStr for Charset {
15    type Err = String;
16
17    fn from_str(s: &str) -> Result<Self, Self::Err> {
18        match s {
19            "utf8" => Ok(Self::Utf8),
20            "ascii" => Ok(Self::Ascii),
21            s => Err(format!("invalid charset: {s}")),
22        }
23    }
24}
25
26/// Options to configure formatting
27#[derive(Clone, Debug, Default)]
28pub struct TreeOptions {
29    /// The style of characters to use.
30    pub charset: Charset,
31    /// If `true`, duplicate imports will be repeated.
32    /// If `false`, duplicates are suffixed with `(*)`, and their imports
33    /// won't be shown.
34    pub no_dedupe: bool,
35}
36
37/// Internal helper type for symbols
38struct Symbols {
39    down: &'static str,
40    tee: &'static str,
41    ell: &'static str,
42    right: &'static str,
43}
44
45static UTF8_SYMBOLS: Symbols = Symbols { down: "│", tee: "├", ell: "└", right: "─" };
46
47static ASCII_SYMBOLS: Symbols = Symbols { down: "|", tee: "|", ell: "`", right: "-" };
48
49pub fn print<D: ParsedSource>(
50    graph: &Graph<D>,
51    opts: &TreeOptions,
52    out: &mut dyn Write,
53) -> io::Result<()> {
54    let symbols = match opts.charset {
55        Charset::Utf8 => &UTF8_SYMBOLS,
56        Charset::Ascii => &ASCII_SYMBOLS,
57    };
58
59    // used to determine whether to display `(*)`
60    let mut visited_imports = HashSet::new();
61
62    // A stack of bools used to determine where | symbols should appear
63    // when printing a line.
64    let mut levels_continue = Vec::new();
65    // used to detect dependency cycles when --no-dedupe is used.
66    // contains a `Node` for each level.
67    let mut write_stack = Vec::new();
68
69    for (node_index, _) in graph.input_nodes().enumerate() {
70        print_node(
71            graph,
72            node_index,
73            symbols,
74            opts.no_dedupe,
75            &mut visited_imports,
76            &mut levels_continue,
77            &mut write_stack,
78            out,
79        )?;
80    }
81
82    Ok(())
83}
84
85#[allow(clippy::too_many_arguments)]
86fn print_node<D: ParsedSource>(
87    graph: &Graph<D>,
88    node_index: usize,
89    symbols: &Symbols,
90    no_dedupe: bool,
91    visited_imports: &mut HashSet<usize>,
92    levels_continue: &mut Vec<bool>,
93    write_stack: &mut Vec<usize>,
94    out: &mut dyn Write,
95) -> io::Result<()> {
96    let new_node = no_dedupe || visited_imports.insert(node_index);
97
98    if let Some((last_continues, rest)) = levels_continue.split_last() {
99        for continues in rest {
100            let c = if *continues { symbols.down } else { " " };
101            write!(out, "{c}   ")?;
102        }
103
104        let c = if *last_continues { symbols.tee } else { symbols.ell };
105        write!(out, "{0}{1}{1} ", c, symbols.right)?;
106    }
107
108    let in_cycle = write_stack.contains(&node_index);
109    // if this node does not have any outgoing edges, don't include the (*)
110    // since there isn't really anything "deduplicated", and it generally just
111    // adds noise.
112    let has_deps = graph.has_outgoing_edges(node_index);
113    let star = if (new_node && !in_cycle) || !has_deps { "" } else { " (*)" };
114
115    writeln!(out, "{}{star}", graph.display_node(node_index))?;
116
117    if !new_node || in_cycle {
118        return Ok(());
119    }
120    write_stack.push(node_index);
121
122    print_imports(
123        graph,
124        node_index,
125        symbols,
126        no_dedupe,
127        visited_imports,
128        levels_continue,
129        write_stack,
130        out,
131    )?;
132
133    write_stack.pop();
134
135    Ok(())
136}
137
138/// Prints all the imports of a node
139#[allow(clippy::too_many_arguments)]
140fn print_imports<D: ParsedSource>(
141    graph: &Graph<D>,
142    node_index: usize,
143    symbols: &Symbols,
144    no_dedupe: bool,
145    visited_imports: &mut HashSet<usize>,
146    levels_continue: &mut Vec<bool>,
147    write_stack: &mut Vec<usize>,
148    out: &mut dyn Write,
149) -> io::Result<()> {
150    let imports = graph.imported_nodes(node_index);
151    if imports.is_empty() {
152        return Ok(());
153    }
154
155    let mut iter = imports.iter().peekable();
156
157    while let Some(import) = iter.next() {
158        levels_continue.push(iter.peek().is_some());
159        print_node(
160            graph,
161            *import,
162            symbols,
163            no_dedupe,
164            visited_imports,
165            levels_continue,
166            write_stack,
167            out,
168        )?;
169        levels_continue.pop();
170    }
171
172    Ok(())
173}