Skip to content

Commit

Permalink
Add crystal tool dependencies (#13613)
Browse files Browse the repository at this point in the history
Co-authored-by: Quinton Miller <[email protected]>
  • Loading branch information
straight-shoota and HertzDevil authored Jul 4, 2023
1 parent 03301a5 commit 8ddf285
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 24 deletions.
29 changes: 23 additions & 6 deletions src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Crystal::Command
expand show macro expansion for given location
format format project, directories and/or files
hierarchy show type hierarchy
dependencies show file dependency tree
implementations show implementations for given call in location
types show type of main variables
--help, -h show this help
Expand Down Expand Up @@ -181,6 +182,9 @@ class Crystal::Command
when "hierarchy".starts_with?(tool)
options.shift
hierarchy
when "dependencies".starts_with?(tool)
options.shift
dependencies
when "implementations".starts_with?(tool)
options.shift
implementations
Expand Down Expand Up @@ -350,7 +354,7 @@ class Crystal::Command

private def create_compiler(command, no_codegen = false, run = false,
hierarchy = false, cursor_command = false,
single_file = false)
single_file = false, dependencies = false)
compiler = new_compiler
compiler.progress_tracker = @progress_tracker
link_flags = [] of String
Expand Down Expand Up @@ -406,8 +410,14 @@ class Crystal::Command
end
end

opts.on("-f text|json", "--format text|json", "Output format text (default) or json") do |f|
output_format = f
if dependencies
opts.on("-f tree|flat", "--format tree|flat", "Output format tree (default) or flat") do |f|
output_format = f
end
else
opts.on("-f text|json", "--format text|json", "Output format text (default) or json") do |f|
output_format = f
end
end

opts.on("--error-trace", "Show full error trace") do
Expand Down Expand Up @@ -543,9 +553,16 @@ class Crystal::Command
end
end

output_format ||= "text"
unless output_format.in?("text", "json")
error "You have input an invalid format, only text and JSON are supported"
if dependencies
output_format ||= "tree"
unless output_format.in?("tree", "flat")
error "You have input an invalid format, only tree and flat are supported"
end
else
output_format ||= "text"
unless output_format.in?("text", "json")
error "You have input an invalid format, only text and JSON are supported"
end
end

error "maximum number of threads cannot be lower than 1" if compiler.n_threads < 1
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ module Crystal
# Whether to link statically
property? static = false

property dependency_printer : DependencyPrinter? = nil

# Program that was created for the last compilation.
property! program : Program

Expand Down
16 changes: 16 additions & 0 deletions src/compiler/crystal/program.cr
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,22 @@ module Crystal
recorded_requires << RecordedRequire.new(filename, relative_to)
end

def run_requires(node : Require, filenames) : Nil
dependency_printer = compiler.try(&.dependency_printer)

filenames.each do |filename|
unseen_file = requires.add?(filename)

dependency_printer.try(&.enter_file(filename, unseen_file))

if unseen_file
yield filename
end

dependency_printer.try(&.leave_file)
end
end

# Finds *filename* in the configured CRYSTAL_PATH for this program,
# relative to *relative_to*.
def find_in_path(filename, relative_to = nil) : Array(String)?
Expand Down
41 changes: 23 additions & 18 deletions src/compiler/crystal/semantic/semantic_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,11 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor

if filenames
nodes = Array(ASTNode).new(filenames.size)
filenames.each do |filename|
if @program.requires.add?(filename)
parser = @program.new_parser(File.read(filename))
parser.filename = filename
parser.wants_doc = @program.wants_doc?
begin
parsed_nodes = parser.parse
parsed_nodes = @program.normalize(parsed_nodes, inside_exp: inside_exp?)
# We must type the node immediately, in case a file requires another
# *before* one of the files in `filenames`
parsed_nodes.accept self
rescue ex : CodeError
node.raise "while requiring \"#{node.string}\"", ex
rescue ex
raise Error.new "while requiring \"#{node.string}\"", ex
end
nodes << FileNode.new(parsed_nodes, filename)
end

@program.run_requires(node, filenames) do |filename|
nodes << require_file(node, filename)
end

expanded = Expressions.from(nodes)
else
expanded = Nop.new
Expand All @@ -98,6 +84,25 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor
false
end

private def require_file(node : Require, filename : String)
parser = @program.new_parser(File.read(filename))
parser.filename = filename
parser.wants_doc = @program.wants_doc?
begin
parsed_nodes = parser.parse
parsed_nodes = @program.normalize(parsed_nodes, inside_exp: inside_exp?)
# We must type the node immediately, in case a file requires another
# *before* one of the files in `filenames`
parsed_nodes.accept self
rescue ex : CodeError
node.raise "while requiring \"#{node.string}\"", ex
rescue ex
raise Error.new "while requiring \"#{node.string}\"", ex
end

FileNode.new(parsed_nodes, filename)
end

def visit(node : ClassDef)
check_outside_exp node, "declare class"
pushing_type(node.resolved_type) do
Expand Down
45 changes: 45 additions & 0 deletions src/compiler/crystal/tools/dependencies.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require "set"
require "colorize"
require "../syntax/ast"

class Crystal::Command
private def dependencies
config = create_compiler "tool dependencies", no_codegen: true, dependencies: true

config.compiler.dependency_printer = DependencyPrinter.new(STDOUT, flat: config.output_format == "flat")
config.compiler.top_level_semantic config.sources
end
end

module Crystal
class DependencyPrinter
@depth = 0

def initialize(@io : IO, @flat : Bool = false)
end

def enter_file(filename : String, unseen : Bool)
print_indent
print_file(filename)
unless unseen
@io.print " (duplicate skipped)"
end
@io.puts

@depth += 1
end

def leave_file
@depth -= 1
end

private def print_indent
return if @flat
@io.print " " * @depth if @depth > 0
end

private def print_file(filename)
@io.print ::Path[filename].relative_to?(Dir.current) || filename
end
end
end

0 comments on commit 8ddf285

Please sign in to comment.