ruby_detective 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +52 -0
- data/LICENSE +21 -0
- data/README.md +35 -0
- data/Rakefile +9 -0
- data/bin/.keep +0 -0
- data/bin/ruby_detective +5 -0
- data/lib/.keep +0 -0
- data/lib/ruby_detective.rb +19 -23
- data/lib/ruby_detective/ast/file_parser.rb +34 -0
- data/lib/ruby_detective/ast/interpreter.rb +59 -0
- data/lib/ruby_detective/ast/node_factory.rb +77 -0
- data/lib/ruby_detective/ast/nodes/absolute_path_sign_node.rb +11 -0
- data/lib/ruby_detective/ast/nodes/class_declaration_node.rb +38 -0
- data/lib/ruby_detective/ast/nodes/constant_reference_node.rb +41 -0
- data/lib/ruby_detective/ast/nodes/generic_node.rb +87 -0
- data/lib/ruby_detective/ast/nodes/module_declaration_node.rb +21 -0
- data/lib/ruby_detective/ast/nodes/query.rb +82 -0
- data/lib/ruby_detective/ast/nodes/value_node.rb +30 -0
- data/lib/ruby_detective/json_builder.rb +30 -0
- data/lib/ruby_detective/runner.rb +39 -0
- data/lib/ruby_detective/source_representation/data_store.rb +67 -0
- data/lib/ruby_detective/source_representation/dependency_resolver.rb +41 -0
- data/lib/ruby_detective/source_representation/entities/base.rb +46 -0
- data/lib/ruby_detective/source_representation/entities/constant.rb +45 -0
- data/lib/ruby_detective/source_representation/entities/klass.rb +47 -0
- data/lib/ruby_detective/source_representation/query.rb +35 -0
- data/lib/ruby_detective/ui_generator.rb +18 -0
- data/ruby_detective.gemspec +22 -0
- data/views/template.html.erb +218 -0
- metadata +35 -3
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module RubyDetective
|
|
2
|
+
module AST
|
|
3
|
+
module Nodes
|
|
4
|
+
class GenericNode
|
|
5
|
+
attr_reader :ast_node, :children, :file_path, :parent_node
|
|
6
|
+
|
|
7
|
+
def initialize(ast_node, file_path:, parent_node:)
|
|
8
|
+
@ast_node = ast_node
|
|
9
|
+
@file_path = file_path
|
|
10
|
+
@children = []
|
|
11
|
+
@parent_node = parent_node
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def short_namespace
|
|
15
|
+
namespace[0..-2]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def namespace
|
|
19
|
+
build_namespace(self)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def declared_namespace
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def class_declaration_node?
|
|
27
|
+
type == :class
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def module_declaration_node?
|
|
31
|
+
type == :module
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def constant_reference_node?
|
|
35
|
+
type == :constant
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def absolute_path_sign_node?
|
|
39
|
+
type == :absolute_path_sign
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def value_node?
|
|
43
|
+
type == :value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generic_node?
|
|
47
|
+
type == :generic
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def type
|
|
51
|
+
:generic
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def first_line
|
|
55
|
+
# When the node represents something that is not directly in the code
|
|
56
|
+
# the `ast_node.loc.expression` can be nil, and since `.line` is just
|
|
57
|
+
# sugar syntax for `loc.expression.line` it would throw an error.
|
|
58
|
+
ast_node.loc.line rescue nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def last_line
|
|
62
|
+
# When the node represents something that is not directly in the code
|
|
63
|
+
# the `ast_node.loc.expression` can be nil, and since `.last_line` is just
|
|
64
|
+
# sugar syntax for `loc.expression.last_line` it would throw an error.
|
|
65
|
+
ast_node.loc.last_line rescue nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def query
|
|
69
|
+
Query.new(self)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def raw_children
|
|
73
|
+
ast_node.children
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def build_namespace(node, acc = [])
|
|
79
|
+
return acc.flatten.compact if node.nil?
|
|
80
|
+
|
|
81
|
+
acc.prepend(node.declared_namespace)
|
|
82
|
+
build_namespace(node.parent_node, acc)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module RubyDetective
|
|
2
|
+
module AST
|
|
3
|
+
module Nodes
|
|
4
|
+
class ModuleDeclarationNode < GenericNode
|
|
5
|
+
MODULE_NAME_NODE_INDEX = 0
|
|
6
|
+
def module_name
|
|
7
|
+
children[MODULE_NAME_NODE_INDEX].constant_name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# TODO: Add support for inline namespacing (ex class Foo::Bar)
|
|
11
|
+
def declared_namespace
|
|
12
|
+
[module_name]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def type
|
|
16
|
+
:module
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module RubyDetective
|
|
2
|
+
module AST
|
|
3
|
+
module Nodes
|
|
4
|
+
class Query
|
|
5
|
+
attr_reader :node
|
|
6
|
+
|
|
7
|
+
def initialize(node)
|
|
8
|
+
@node = node
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# TODO: accept multiple criteria
|
|
12
|
+
def where(criteria = {})
|
|
13
|
+
case
|
|
14
|
+
when criteria.key?(:type)
|
|
15
|
+
type_validation_function = ->(node) { node.type == criteria[:type] }
|
|
16
|
+
deep_search(node, [type_validation_function])
|
|
17
|
+
else
|
|
18
|
+
deep_search(node)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# TODO: ignore constant definitions, only return constant references
|
|
23
|
+
def constant_references(where: {})
|
|
24
|
+
constants = deep_search(node, [:constant_reference_node?])
|
|
25
|
+
|
|
26
|
+
case
|
|
27
|
+
when where.key?(:namespace)
|
|
28
|
+
constants.select { |c| c.namespace.include?(where[:namespace].to_sym) }
|
|
29
|
+
else
|
|
30
|
+
constants
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# TODO: ignore constant definitions, only return constant references
|
|
35
|
+
# This finds all constants, ignoring the nested ones.
|
|
36
|
+
# For example:
|
|
37
|
+
# The "Foo::Bar" code contain two constants, but this method will only bring
|
|
38
|
+
# up one (the Bar one), with access to it's full path.
|
|
39
|
+
def top_level_constant_references(where: {})
|
|
40
|
+
constants = deep_search(node, [:constant_reference_node?, :top_level_constant?])
|
|
41
|
+
|
|
42
|
+
case
|
|
43
|
+
when where.key?(:namespace)
|
|
44
|
+
constants.select { |c| c.namespace.include?(where[:namespace].to_sym) }
|
|
45
|
+
else
|
|
46
|
+
constants
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def class_declarations
|
|
51
|
+
deep_search(node, [:class_declaration_node?])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def deep_search(node, validation_methods = [], acc: [])
|
|
57
|
+
return if node.value_node?
|
|
58
|
+
|
|
59
|
+
validation_result = validation_methods.map do |validation_method|
|
|
60
|
+
if validation_method.is_a?(Symbol)
|
|
61
|
+
node.respond_to?(validation_method) && node.public_send(validation_method)
|
|
62
|
+
elsif validation_method.is_a?(Proc)
|
|
63
|
+
begin
|
|
64
|
+
validation_method.call(node)
|
|
65
|
+
rescue NoMethodError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
raise ArgumentError, "Unexpected validation method data type"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Only appends the node to the results if all validations passed
|
|
74
|
+
acc << node if validation_result.all?
|
|
75
|
+
|
|
76
|
+
node.children.each { |child| send(__method__, child, validation_methods, acc: acc) }
|
|
77
|
+
acc.uniq
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RubyDetective
|
|
2
|
+
module AST
|
|
3
|
+
module Nodes
|
|
4
|
+
class ValueNode < GenericNode
|
|
5
|
+
attr_reader :value
|
|
6
|
+
|
|
7
|
+
def initialize(value, *args)
|
|
8
|
+
super(value, *args)
|
|
9
|
+
@value = value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def type
|
|
13
|
+
:value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def first_line
|
|
17
|
+
parent_node.first_line
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def last_line
|
|
21
|
+
parent_node.last_line
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def raw_children
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module RubyDetective
|
|
4
|
+
class JSONBuilder
|
|
5
|
+
attr_reader :classes
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
data_store = SourceRepresentation::DataStore.instance
|
|
9
|
+
@classes = data_store.classes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.build(*args)
|
|
13
|
+
new(*args).build
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build
|
|
17
|
+
classes_data_as_json = classes.map do |c|
|
|
18
|
+
{
|
|
19
|
+
name: c.name,
|
|
20
|
+
full_name: c.path_as_text,
|
|
21
|
+
namespace: c.namespace_as_text,
|
|
22
|
+
lines_of_code: c.lines_of_code,
|
|
23
|
+
dependencies: c.dependencies.map(&:path_as_text),
|
|
24
|
+
dependents: c.dependents.map(&:path_as_text),
|
|
25
|
+
file_path: c.file_path
|
|
26
|
+
}
|
|
27
|
+
end.to_json
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "parser/current"
|
|
2
|
+
|
|
3
|
+
module RubyDetective
|
|
4
|
+
class Runner
|
|
5
|
+
attr_reader :project_path, :classes, :modules
|
|
6
|
+
|
|
7
|
+
def initialize(project_path)
|
|
8
|
+
if [nil, "", " "].include? project_path
|
|
9
|
+
@project_path = "."
|
|
10
|
+
else
|
|
11
|
+
@project_path = project_path
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
puts "Processing files..."
|
|
17
|
+
Dir.glob("#{project_path}/**/*.rb") do |file_path|
|
|
18
|
+
AST::FileParser.new(file_path, project_path).parse
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
puts "Finding dependencies..."
|
|
22
|
+
SourceRepresentation::DataStore.instance.resolve_dependencies
|
|
23
|
+
|
|
24
|
+
if ENV["ENV"] == "development"
|
|
25
|
+
puts "Generating output .json file..."
|
|
26
|
+
json = ::RubyDetective::JSONBuilder.build
|
|
27
|
+
|
|
28
|
+
output_file_path = "ui/src/data.json"
|
|
29
|
+
File.delete(output_file_path) if File.exist?(output_file_path)
|
|
30
|
+
File.open(output_file_path, "w") { |file| file << json }
|
|
31
|
+
else
|
|
32
|
+
puts "Generating output HTML file..."
|
|
33
|
+
UIGenerator.generate
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "Done!"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require "singleton"
|
|
2
|
+
|
|
3
|
+
# This class is used as a database of sorts during the
|
|
4
|
+
# analysis execution.
|
|
5
|
+
module RubyDetective
|
|
6
|
+
module SourceRepresentation
|
|
7
|
+
class DataStore
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
attr_accessor :classes, :constants
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@classes = []
|
|
14
|
+
@constants = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def query
|
|
18
|
+
Query.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear!
|
|
22
|
+
initialize
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inspect
|
|
26
|
+
"#<RubyDetective::SourceRepresentation::DataStore>"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_dependencies
|
|
30
|
+
DependencyResolver.resolve_and_populate_store
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def register_class(name, namespace, inheritance_class_name:, file_path:, first_line:, last_line:)
|
|
34
|
+
klass = Entities::Klass.new(
|
|
35
|
+
name,
|
|
36
|
+
namespace,
|
|
37
|
+
inheritance_class_name: inheritance_class_name,
|
|
38
|
+
file_path: file_path,
|
|
39
|
+
first_line: first_line,
|
|
40
|
+
last_line: last_line
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
existing_class = query.classes(where: { path: klass.path }).first
|
|
44
|
+
|
|
45
|
+
if existing_class
|
|
46
|
+
existing_class.merge(klass)
|
|
47
|
+
existing_class
|
|
48
|
+
else
|
|
49
|
+
@classes << klass
|
|
50
|
+
klass
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def register_constant(name, namespace, file_path:, caller:, refers_to: nil)
|
|
55
|
+
constant = Entities::Constant.new(
|
|
56
|
+
name,
|
|
57
|
+
namespace,
|
|
58
|
+
caller: caller,
|
|
59
|
+
refers_to: refers_to,
|
|
60
|
+
file_path: file_path
|
|
61
|
+
)
|
|
62
|
+
@constants << constant
|
|
63
|
+
constant
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module RubyDetective
|
|
2
|
+
module SourceRepresentation
|
|
3
|
+
class DependencyResolver
|
|
4
|
+
attr_reader :classes
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@classes = DataStore.instance.classes
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.resolve_and_populate_store
|
|
11
|
+
new.resolve_and_populate_store
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resolve_and_populate_store
|
|
15
|
+
register_dependencies_and_dependents
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def register_dependencies_and_dependents
|
|
22
|
+
classes.each do |klass|
|
|
23
|
+
klass.constants.each do |constant|
|
|
24
|
+
referred_class = find_referred_class(constant)
|
|
25
|
+
next if referred_class.nil?
|
|
26
|
+
|
|
27
|
+
constant.register_referred_class(referred_class)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_referred_class(constant)
|
|
33
|
+
classes.select do |klass|
|
|
34
|
+
constant.possible_paths_of_referenced_entity.find do |possible_path|
|
|
35
|
+
klass.path == possible_path
|
|
36
|
+
end
|
|
37
|
+
end.compact.first
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module RubyDetective
|
|
2
|
+
module SourceRepresentation
|
|
3
|
+
module Entities
|
|
4
|
+
class Base
|
|
5
|
+
ROOT_SIGN_SYMBOL = :"::"
|
|
6
|
+
|
|
7
|
+
def absolute_path?
|
|
8
|
+
namespace.first == ROOT_SIGN_SYMBOL
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def path
|
|
12
|
+
namespace + [name]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def path_without_root_sign
|
|
16
|
+
namespace_without_root_sign + [name]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Removes the :"::" symbol from the namespace
|
|
20
|
+
def namespace_without_root_sign
|
|
21
|
+
if absolute_path?
|
|
22
|
+
namespace[1..-1]
|
|
23
|
+
else
|
|
24
|
+
namespace
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def path_as_text
|
|
29
|
+
if absolute_path?
|
|
30
|
+
"::" + path_without_root_sign.join("::")
|
|
31
|
+
else
|
|
32
|
+
path.join("::")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def namespace_as_text
|
|
37
|
+
if absolute_path?
|
|
38
|
+
"::" + namespace_without_root_sign.join("::")
|
|
39
|
+
else
|
|
40
|
+
namespace.join("::")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|