lib/spoom/file_tree.rb in spoom-1.2.0 vs lib/spoom/file_tree.rb in spoom-1.2.1

- old
+ new

@@ -4,17 +4,13 @@ module Spoom # Build a file hierarchy from a set of file paths. class FileTree extend T::Sig - sig { returns(T.nilable(String)) } - attr_reader :strip_prefix - - sig { params(paths: T::Enumerable[String], strip_prefix: T.nilable(String)).void } - def initialize(paths = [], strip_prefix: nil) + sig { params(paths: T::Enumerable[String]).void } + def initialize(paths = []) @roots = T.let({}, T::Hash[String, Node]) - @strip_prefix = strip_prefix add_paths(paths) end # Add all `paths` to the tree sig { params(paths: T::Enumerable[String]).void } @@ -25,12 +21,10 @@ # Add a `path` to the tree # # This will create all nodes until the root of `path`. sig { params(path: String).returns(Node) } def add_path(path) - prefix = @strip_prefix - path = path.delete_prefix("#{prefix}/") if prefix parts = path.split("/") if path.empty? || parts.size == 1 return @roots[path] ||= Node.new(parent: nil, name: path) end @@ -47,49 +41,57 @@ end # All the nodes in this tree sig { returns(T::Array[Node]) } def nodes - all_nodes = [] - @roots.values.each { |root| collect_nodes(root, all_nodes) } - all_nodes + v = CollectNodes.new + v.visit_tree(self) + v.nodes end # All the paths in this tree sig { returns(T::Array[String]) } def paths - nodes.collect(&:path) + nodes.map(&:path) end - sig do - params( - out: T.any(IO, StringIO), - show_strictness: T::Boolean, - colors: T::Boolean, - indent_level: Integer, - ).void + # Return a map of strictnesses for each node in the tree + sig { params(context: Context).returns(T::Hash[Node, T.nilable(String)]) } + def nodes_strictnesses(context) + v = CollectStrictnesses.new(context) + v.visit_tree(self) + v.strictnesses end - def print(out: $stdout, show_strictness: true, colors: true, indent_level: 0) - printer = TreePrinter.new( - tree: self, - out: out, - show_strictness: show_strictness, - colors: colors, - indent_level: indent_level, - ) - printer.print_tree + + # Return a map of typing scores for each node in the tree + sig { params(context: Context).returns(T::Hash[Node, Float]) } + def nodes_strictness_scores(context) + v = CollectScores.new(context) + v.visit_tree(self) + v.scores end - private + # Return a map of typing scores for each path in the tree + sig { params(context: Context).returns(T::Hash[String, Float]) } + def paths_strictness_scores(context) + nodes_strictness_scores(context).map { |node, score| [node.path, score] }.to_h + end - sig { params(node: FileTree::Node, collected_nodes: T::Array[Node]).returns(T::Array[Node]) } - def collect_nodes(node, collected_nodes = []) - collected_nodes << node - node.children.values.each { |child| collect_nodes(child, collected_nodes) } - collected_nodes + sig { params(out: T.any(IO, StringIO), colors: T::Boolean).void } + def print(out: $stdout, colors: true) + printer = Printer.new({}, out: out, colors: colors) + printer.visit_tree(self) end + sig { params(context: Context, out: T.any(IO, StringIO), colors: T::Boolean).void } + def print_with_strictnesses(context, out: $stdout, colors: true) + strictnesses = nodes_strictnesses(context) + + printer = Printer.new(strictnesses, out: out, colors: colors) + printer.visit_tree(self) + end + # A node representing either a file or a directory inside a FileTree class Node < T::Struct extend T::Sig # Node parent or `nil` if the node is a root one @@ -109,79 +111,162 @@ "#{parent.path}/#{name}" end end + # An abstract visitor for FileTree + class Visitor + extend T::Sig + extend T::Helpers + + abstract! + + sig { params(tree: FileTree).void } + def visit_tree(tree) + visit_nodes(tree.roots) + end + + sig { params(node: FileTree::Node).void } + def visit_node(node) + visit_nodes(node.children.values) + end + + sig { params(nodes: T::Array[FileTree::Node]).void } + def visit_nodes(nodes) + nodes.each { |node| visit_node(node) } + end + end + + # A visitor that collects all the nodes in a tree + class CollectNodes < Visitor + extend T::Sig + + sig { returns(T::Array[FileTree::Node]) } + attr_reader :nodes + + sig { void } + def initialize + super() + @nodes = T.let([], T::Array[FileTree::Node]) + end + + sig { override.params(node: FileTree::Node).void } + def visit_node(node) + @nodes << node + super + end + end + + # A visitor that collects the strictness of each node in a tree + class CollectStrictnesses < Visitor + extend T::Sig + + sig { returns(T::Hash[Node, T.nilable(String)]) } + attr_reader :strictnesses + + sig { params(context: Context).void } + def initialize(context) + super() + @context = context + @strictnesses = T.let({}, T::Hash[Node, T.nilable(String)]) + end + + sig { override.params(node: FileTree::Node).void } + def visit_node(node) + path = node.path + @strictnesses[node] = @context.read_file_strictness(path) if @context.file?(path) + + super + end + end + + # A visitor that collects the typing score of each node in a tree + class CollectScores < CollectStrictnesses + extend T::Sig + + sig { returns(T::Hash[Node, Float]) } + attr_reader :scores + + sig { params(context: Context).void } + def initialize(context) + super + @context = context + @scores = T.let({}, T::Hash[Node, Float]) + end + + sig { override.params(node: FileTree::Node).void } + def visit_node(node) + super + + @scores[node] = node_score(node) + end + + private + + sig { params(node: Node).returns(Float) } + def node_score(node) + if @context.file?(node.path) + strictness_score(@strictnesses[node]) + else + node.children.values.sum { |child| @scores.fetch(child, 0.0) } / node.children.size.to_f + end + end + + sig { params(strictness: T.nilable(String)).returns(Float) } + def strictness_score(strictness) + case strictness + when "true", "strict", "strong" + 1.0 + else + 0.0 + end + end + end + # An internal class used to print a FileTree # # See `FileTree#print` - class TreePrinter < Spoom::Printer + class Printer < Visitor extend T::Sig - sig { returns(FileTree) } - attr_reader :tree - sig do params( - tree: FileTree, + strictnesses: T::Hash[FileTree::Node, T.nilable(String)], out: T.any(IO, StringIO), - show_strictness: T::Boolean, colors: T::Boolean, - indent_level: Integer, ).void end - def initialize(tree:, out: $stdout, show_strictness: true, colors: true, indent_level: 0) - super(out: out, colors: colors, indent_level: indent_level) - @tree = tree - @show_strictness = show_strictness + def initialize(strictnesses, out: $stdout, colors: true) + super() + @strictnesses = strictnesses + @colors = colors + @printer = T.let(Spoom::Printer.new(out: out, colors: colors), Spoom::Printer) end - sig { void } - def print_tree - print_nodes(tree.roots) - end - - sig { params(node: FileTree::Node).void } - def print_node(node) - printt + sig { override.params(node: FileTree::Node).void } + def visit_node(node) + @printer.printt if node.children.empty? - if @show_strictness - strictness = node_strictness(node) - if @colors - print_colored(node.name, strictness_color(strictness)) - elsif strictness - print("#{node.name} (#{strictness})") - else - print(node.name.to_s) - end + strictness = @strictnesses[node] + if @colors + @printer.print_colored(node.name, strictness_color(strictness)) + elsif strictness + @printer.print("#{node.name} (#{strictness})") else - print(node.name.to_s) + @printer.print(node.name.to_s) end - print("\n") + @printer.print("\n") else - print_colored(node.name, Color::BLUE) - print("/") - printn - indent - print_nodes(node.children.values) - dedent + @printer.print_colored(node.name, Color::BLUE) + @printer.print("/") + @printer.printn + @printer.indent + super + @printer.dedent end end - sig { params(nodes: T::Array[FileTree::Node]).void } - def print_nodes(nodes) - nodes.each { |node| print_node(node) } - end - private - - sig { params(node: FileTree::Node).returns(T.nilable(String)) } - def node_strictness(node) - path = node.path - prefix = tree.strip_prefix - path = "#{prefix}/#{path}" if prefix - Spoom::Sorbet::Sigils.file_strictness(path) - end sig { params(strictness: T.nilable(String)).returns(Color) } def strictness_color(strictness) case strictness when "false"