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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +52 -0
  6. data/LICENSE +21 -0
  7. data/README.md +35 -0
  8. data/Rakefile +9 -0
  9. data/bin/.keep +0 -0
  10. data/bin/ruby_detective +5 -0
  11. data/lib/.keep +0 -0
  12. data/lib/ruby_detective.rb +19 -23
  13. data/lib/ruby_detective/ast/file_parser.rb +34 -0
  14. data/lib/ruby_detective/ast/interpreter.rb +59 -0
  15. data/lib/ruby_detective/ast/node_factory.rb +77 -0
  16. data/lib/ruby_detective/ast/nodes/absolute_path_sign_node.rb +11 -0
  17. data/lib/ruby_detective/ast/nodes/class_declaration_node.rb +38 -0
  18. data/lib/ruby_detective/ast/nodes/constant_reference_node.rb +41 -0
  19. data/lib/ruby_detective/ast/nodes/generic_node.rb +87 -0
  20. data/lib/ruby_detective/ast/nodes/module_declaration_node.rb +21 -0
  21. data/lib/ruby_detective/ast/nodes/query.rb +82 -0
  22. data/lib/ruby_detective/ast/nodes/value_node.rb +30 -0
  23. data/lib/ruby_detective/json_builder.rb +30 -0
  24. data/lib/ruby_detective/runner.rb +39 -0
  25. data/lib/ruby_detective/source_representation/data_store.rb +67 -0
  26. data/lib/ruby_detective/source_representation/dependency_resolver.rb +41 -0
  27. data/lib/ruby_detective/source_representation/entities/base.rb +46 -0
  28. data/lib/ruby_detective/source_representation/entities/constant.rb +45 -0
  29. data/lib/ruby_detective/source_representation/entities/klass.rb +47 -0
  30. data/lib/ruby_detective/source_representation/query.rb +35 -0
  31. data/lib/ruby_detective/ui_generator.rb +18 -0
  32. data/ruby_detective.gemspec +22 -0
  33. data/views/template.html.erb +218 -0
  34. 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