Skip to content

Commit

Permalink
Add namespaces tool + specs
Browse files Browse the repository at this point in the history
Based on hierarchy command
  • Loading branch information
nobodywasishere committed Jun 14, 2024
1 parent a269524 commit bfbbc9b
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 0 deletions.
139 changes: 139 additions & 0 deletions spec/compiler/crystal/tools/namespaces_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
require "../../../spec_helper"

private def assert_text_namespaces(source, filter, expected, *, file = __FILE__, line = __LINE__)
program = semantic(source).program
output = String.build { |io| Crystal.print_namespaces(program, io, filter, "text") }
output.should eq(expected), file: file, line: line
end

private def assert_json_namespaces(source, filter, expected, *, file = __FILE__, line = __LINE__)
program = semantic(source).program
output = String.build { |io| Crystal.print_namespaces(program, io, filter, "json") }
JSON.parse(output).should eq(JSON.parse(expected)), file: file, line: line
end

describe Crystal::TextNamespacesPrinter do
it "works" do
assert_text_namespaces <<-CRYSTAL, "Foo", <<-EOS
class Foo
annotation Baz
end
module Fizz
abstract struct Buzz
end
end
end
class Foo::Bar < Foo
end
struct Foo::Fizz::Buzz
end
CRYSTAL
- class Foo
@ :1:1
- class Foo::Bar
@ :11:1
- annotation Foo::Baz
@ :2:3
- module Foo::Fizz
@ :5:3
- struct Foo::Fizz::Buzz
@ :6:5
@ :14:1\n
EOS
end
end

describe Crystal::JSONNamespacesPrinter do
it "works" do
assert_json_namespaces <<-CRYSTAL, "Foo", <<-JSON
class Foo
annotation Baz
end
module Fizz
abstract struct Buzz
end
end
end
class Foo::Bar < Foo
end
struct Foo::Fizz::Buzz
end
CRYSTAL
[
{"name": "Foo", "kind": "class", "locations": [":1:1"]},
{"name": "Foo::Bar", "kind": "class", "locations": [":11:1"]},
{"name": "Foo::Baz", "kind": "annotation", "locations": [":2:3"]},
{"name": "Foo::Fizz", "kind": "module", "locations": [":5:3"]},
{
"name": "Foo::Fizz::Buzz",
"kind": "struct",
"locations": [":6:5", ":14:1"]
}
]
JSON
end

it "supports macros" do
assert_json_namespaces <<-CRYSTAL, "Foo", <<-JSON
macro my_macro
class Foo
annotation Baz
end
module Fizz
abstract struct Buzz
end
end
end
end
my_macro
class Foo::Bar < Foo
end
struct Foo::Fizz::Buzz
end
CRYSTAL
[
{"name": "Foo", "kind": "class", "locations": [":2:3"]},
{"name": "Foo::Bar", "kind": "class", "locations": [":15:1"]},
{"name": "Foo::Baz", "kind": "annotation", "locations": [":3:5"]},
{"name": "Foo::Fizz", "kind": "module", "locations": [":6:5"]},
{
"name": "Foo::Fizz::Buzz",
"kind": "struct",
"locations": [":7:7", ":18:1"]
}
]
JSON
end

it "supports enum values" do
assert_json_namespaces <<-CRYSTAL, "Foo", <<-JSON
class Foo
enum Baz
VALUE_1
VALUE_2
VALUE_3
VALUE_4
end
end
CRYSTAL
[
{"name": "Foo", "kind": "class", "locations": [":1:1"]},
{"name": "Foo::Baz", "kind": "enum", "locations": [":2:3"]},
{"name": "Foo::Baz::VALUE_1", "kind": "const", "locations": [":3:5"]},
{"name": "Foo::Baz::VALUE_2", "kind": "const", "locations": [":4:5"]},
{"name": "Foo::Baz::VALUE_3", "kind": "const", "locations": [":5:5"]},
{"name": "Foo::Baz::VALUE_4", "kind": "const", "locations": [":6:5"]}
]
JSON
end
end
11 changes: 11 additions & 0 deletions src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Crystal::Command
implementations show implementations for given call in location
unreachable show methods that are never called
types show type of main variables
namespaces show all class/struct/module/annotation/enum/etc names
--help, -h show this help
USAGE

Expand Down Expand Up @@ -187,6 +188,9 @@ class Crystal::Command
when "hierarchy".starts_with?(tool)
options.shift
hierarchy
when "namespaces".starts_with?(tool)
options.shift
namespaces
when "dependencies".starts_with?(tool)
options.shift
dependencies
Expand Down Expand Up @@ -223,6 +227,13 @@ class Crystal::Command
end
end

private def namespaces
config, result = compile_no_codegen "tool namespaces", hierarchy: true, top_level: true, wants_doc: true
@progress_tracker.stage("Tool (namespaces)") do
Crystal.print_namespaces result.program, STDOUT, config.hierarchy_exp, config.output_format
end
end

private def run_command(single_file = false)
config = create_compiler "run", run: true, single_file: single_file
if config.specified_output
Expand Down
180 changes: 180 additions & 0 deletions src/compiler/crystal/tools/print_namespaces.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
require "set"
require "colorize"
require "../syntax/ast"

module Crystal
def self.print_namespaces(program, io, exp, format)
case format
when "text"
TextNamespacesPrinter.new(program, io, exp).print_all
when "json"
JSONNamespacesPrinter.new(program, io, exp).print_all
else
raise "Unknown namespaces format: #{format}"
end
end

abstract class NamespacesPrinter
abstract def print_all

def initialize(@program : Program, exp : String?)
@exp = exp ? Regex.new(exp) : nil
end

private def collect_types(types : Array(Type))
collected = Set(NamedType).new

types.each do |type|
next unless type.is_a?(NamedType)

collected.add(type)

if type.struct? || type.class? || type.metaclass?
collected.concat collect_types(type.subclasses)
end

if sub_types = type.types?
collected.concat collect_types(sub_types.values)
end
end

collected
end

protected def location_to_s(loc : Location)
f = loc.filename
case f
when String
line = loc.line_number
column = loc.column_number
filename = f
when VirtualFile
macro_location = f.macro.location.not_nil!
filename = macro_location.filename.to_s
line = macro_location.line_number + loc.line_number
column = loc.column_number
else
raise "not implemented"
end

"#{filename}:#{line}:#{column}"
end
end

class TextNamespacesPrinter < NamespacesPrinter
def initialize(program : Program, @io : IO, exp : String?)
super(program, exp)
end

def print_all
types = collect_types(@program.types.values)
types = types.to_a.sort_by(&.full_name)

types.each do |type|
print_type(type)
end
end

def print_type(type : NamedType)
return if (exp = @exp) && (exp !~ type.full_name)
return if type.is_a?(MetaclassType)

@io << "- "
@io << case type
in Const
"const"
in .struct?
"struct"
in .class?
"class"
in .module?
"module"
in AliasType
"alias"
in EnumType
"enum"
in NoReturnType, VoidType
"struct"
in AnnotationType
"annotation"
in LibType
"module"
in TypeDefType
"typedef"
in MetaclassType, NamedType
""
end

@io << " " << type.full_name

if locations = type.locations
@io << "\n"
locations.each do |loc|
@io << " @ " << location_to_s(loc) << "\n"
end
end
end
end

class JSONNamespacesPrinter < NamespacesPrinter
def initialize(program : Program, io : IO, exp : String?)
super(program, exp)
@json = JSON::Builder.new(io)
end

def print_all
types = collect_types(@program.types.values)
types = types.to_a.sort_by(&.full_name)

@json.document do
@json.array do
types.each do |type|
print_type(type)
end
end
end
end

def print_type(type : NamedType)
return if (exp = @exp) && (exp !~ type.full_name)
return if type.is_a?(MetaclassType)

@json.object do
@json.field("name", type.full_name)

case type
in Const
@json.field("kind", "const")
in .struct?
@json.field("kind", "struct")
in .class?
@json.field("kind", "class")
in .module?
@json.field("kind", "module")
in AliasType
@json.field("kind", "alias")
in EnumType
@json.field("kind", "enum")
in NoReturnType, VoidType
@json.field("kind", "struct")
in AnnotationType
@json.field("kind", "annotation")
in LibType
@json.field("kind", "module")
in TypeDefType
@json.field("kind", "typedef")
in MetaclassType, NamedType
end

if locations = type.locations
@json.string("locations")
@json.array do
locations.each do |loc|
@json.string(location_to_s(loc))
end
end
end
end
end
end
end

0 comments on commit bfbbc9b

Please sign in to comment.