ruby_detective 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20b16d18a1225c536e2ec0cd259b20d0d885658a1cb2c1ca93ae42dac1af4f3c
4
- data.tar.gz: f47b00d0f6391ae592c7fd63d1a326d18bbe61ea8f2fa837d43dff4f25074ec7
3
+ metadata.gz: abe0950ab9ca4d1ed6e201099393deba959859cea88d1d2ca3467dacc38b00d9
4
+ data.tar.gz: ea9b99edf95de63499ae29a537d2031e312b2c0b7fa7e37947287dd67a96de3c
5
5
  SHA512:
6
- metadata.gz: 3a3b6cc52488de91de39f1c812ba9a49f040988ff16166b783bad9a6da993054ed464ce2261eacc3010840dd5138752e6eab4abafa14e0fda63d3998ea552df8
7
- data.tar.gz: 5cfdf6e719d1a8df329e1e6e55cfce65268f6eec2d1d162e4b96c6f0c2d2189e1a7ecd6cc354c50f44419ce317c6fa4e3c3a855abf9e27e0d47c47818c9395f0
6
+ metadata.gz: 25e3f035527b125ee423a7b6689889681c935a72c2efe5fe8459cdc649a3050a499421dd005e8f9747b7283a048eda6ff43ca8bfea9439ad5040bfc8935eeb11
7
+ data.tar.gz: f7b9bb40ec643c51c8e4e2d20bf330a2c2b8b68428c03907f85fffa05cd0ddc46057b866e31510492cdffe1d13fff20acf5787639229b94d7008bafe14b307b2
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ tmp/
3
+ spec/examples.txt
4
+ coverage/
5
+ ruby_detective.html
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,52 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ruby_detective (0.0.0)
5
+ parser (~> 2.6.5)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.0)
11
+ coderay (1.1.2)
12
+ diff-lcs (1.3)
13
+ docile (1.3.2)
14
+ json (2.3.0)
15
+ method_source (0.9.2)
16
+ parser (2.6.5.0)
17
+ ast (~> 2.4.0)
18
+ pry (0.12.2)
19
+ coderay (~> 1.1.0)
20
+ method_source (~> 0.9.0)
21
+ rake (13.0.1)
22
+ rspec (3.9.0)
23
+ rspec-core (~> 3.9.0)
24
+ rspec-expectations (~> 3.9.0)
25
+ rspec-mocks (~> 3.9.0)
26
+ rspec-core (3.9.0)
27
+ rspec-support (~> 3.9.0)
28
+ rspec-expectations (3.9.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.9.0)
31
+ rspec-mocks (3.9.0)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.9.0)
34
+ rspec-support (3.9.0)
35
+ simplecov (0.17.1)
36
+ docile (~> 1.1)
37
+ json (>= 1.8, < 3)
38
+ simplecov-html (~> 0.10.0)
39
+ simplecov-html (0.10.2)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ pry
46
+ rake (~> 13.0.1)
47
+ rspec (~> 3.9.0)
48
+ ruby_detective!
49
+ simplecov (~> 0.17.1)
50
+
51
+ BUNDLED WITH
52
+ 1.17.2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Victor A.M. <victor.atmorning@gmail.com.br>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Ruby Detective
2
+ ### Investigating your project dependencies
3
+
4
+ Ruby Detective is a gem that parses your code, finds it's dependencies and outputs a interactive .html file that you can use to explore the dependency network of the code.
5
+
6
+ This is the UI for the Ruby Detective project by the way :smile:
7
+
8
+ ![Preview](docs/preview.png?raw=true)
9
+
10
+ ***:** Due to Ruby metaprogramming super-powers (and by extension Rails heavy use of those) it's unfeasible to find every single dependency, so we can only guarantee that explicit constants will be pointed as dependencies.
11
+
12
+ ## Main features
13
+ - Explorable and interactive network graph of the project dependencies
14
+ - Graph nodes colored by namespace, making it easier to spot contexts
15
+ - Useful information like lines of code, number of dependencies and dependents, etc
16
+ - Fully self-contained .html file that can be easily shared
17
+
18
+ ## Instalation
19
+ ```
20
+ gem install ruby-detective
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```
26
+ cd my-project-folder
27
+ ruby-detective .
28
+ ```
29
+
30
+ This should output an html file at the end that is completely self-contained, and can be shared around with your peers :D
31
+
32
+ ### Some tips
33
+ - Click on a node to bring it's card to the top of the list on the left
34
+ - Click twice on a node to add it to the graph, allowing to navigate through dependencies
35
+ - Use the filters on the right to customize the graph, toggling off the "Show second-level dependency edges" option can be specially useful
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ begin
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :test => :spec
7
+ rescue LoadError
8
+ # no rspec available
9
+ end
data/bin/.keep ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ruby_detective'
4
+ parser = RubyDetective::Runner.new(ARGV[0])
5
+ parser.run
data/lib/.keep ADDED
File without changes
@@ -3,30 +3,26 @@ end
3
3
 
4
4
  require "parser/current"
5
5
 
6
- require_relative "ruby_detective/runner"
7
- require_relative "ruby_detective/json_builder"
8
- require_relative "ruby_detective/ui_generator"
6
+ require "ruby_detective/runner"
7
+ require "ruby_detective/json_builder"
8
+ require "ruby_detective/ui_generator"
9
9
 
10
- require_relative "ruby_detective/source_representation/data_store"
11
- require_relative "ruby_detective/source_representation/query"
12
- require_relative "ruby_detective/source_representation/dependency_resolver"
10
+ require "ruby_detective/source_representation/data_store"
11
+ require "ruby_detective/source_representation/query"
12
+ require "ruby_detective/source_representation/dependency_resolver"
13
13
 
14
- require_relative "ruby_detective/source_representation/entities/base"
15
- require_relative "ruby_detective/source_representation/entities/klass"
16
- require_relative "ruby_detective/source_representation/entities/constant"
14
+ require "ruby_detective/source_representation/entities/base"
15
+ require "ruby_detective/source_representation/entities/klass"
16
+ require "ruby_detective/source_representation/entities/constant"
17
17
 
18
- require_relative "ruby_detective/ast/file_parser"
19
- require_relative "ruby_detective/ast/interpreter"
20
- require_relative "ruby_detective/ast/node_factory"
18
+ require "ruby_detective/ast/file_parser"
19
+ require "ruby_detective/ast/interpreter"
20
+ require "ruby_detective/ast/node_factory"
21
21
 
22
- require_relative "ruby_detective/ast/nodes/query"
23
- require_relative "ruby_detective/ast/nodes/generic_node"
24
- require_relative "ruby_detective/ast/nodes/value_node"
25
- require_relative "ruby_detective/ast/nodes/constant_reference_node"
26
- require_relative "ruby_detective/ast/nodes/class_declaration_node"
27
- require_relative "ruby_detective/ast/nodes/module_declaration_node"
28
- require_relative "ruby_detective/ast/nodes/absolute_path_sign_node"
29
-
30
- if ENV["ENV"] == "development"
31
- require "pry"
32
- end
22
+ require "ruby_detective/ast/nodes/query"
23
+ require "ruby_detective/ast/nodes/generic_node"
24
+ require "ruby_detective/ast/nodes/value_node"
25
+ require "ruby_detective/ast/nodes/constant_reference_node"
26
+ require "ruby_detective/ast/nodes/class_declaration_node"
27
+ require "ruby_detective/ast/nodes/module_declaration_node"
28
+ require "ruby_detective/ast/nodes/absolute_path_sign_node"
@@ -0,0 +1,34 @@
1
+ module RubyDetective
2
+ module AST
3
+ class FileParser
4
+ attr_reader :path, :project_path, :rich_ast
5
+
6
+ def initialize(file_path, project_path)
7
+ @path = file_path
8
+ @project_path = project_path
9
+ end
10
+
11
+ def parse
12
+ code = File.read(path)
13
+
14
+ raw_ast = Parser::CurrentRuby.parse(code)
15
+ return false if raw_ast.nil? # Empty file scenario
16
+
17
+ factory = AST::NodeFactory.new(raw_ast, file_path: clean_path)
18
+ @rich_ast = factory.build
19
+ factory.process_all_children
20
+
21
+ AST::Interpreter.interpret_node_and_populate_store(
22
+ rich_ast,
23
+ clean_path
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def clean_path
30
+ path.sub(project_path, "")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ module RubyDetective
2
+ module AST
3
+ class Interpreter
4
+ attr_reader :rich_ast, :classes, :file_path
5
+
6
+ def initialize(rich_ast, file_path)
7
+ @rich_ast = rich_ast
8
+ @file_path = file_path
9
+ end
10
+
11
+ def self.interpret_node_and_populate_store(*args)
12
+ new(*args).interpret_node_and_populate_store
13
+ end
14
+
15
+ def interpret_node_and_populate_store
16
+ register_classes_and_constants
17
+
18
+ true
19
+ end
20
+
21
+ private
22
+
23
+ def register_classes_and_constants
24
+ rich_ast.query.class_declarations.map do |class_node|
25
+ class_representation = register_class(class_node)
26
+ register_constants_referenced_in_class(class_representation)
27
+ end
28
+ end
29
+
30
+ def register_class(node)
31
+ data_store = SourceRepresentation::DataStore.instance
32
+ data_store.register_class(
33
+ node.class_name,
34
+ node.short_namespace,
35
+ inheritance_class_name: node.inheritance_class_name,
36
+ file_path: node.file_path,
37
+ first_line: node.first_line,
38
+ last_line: node.last_line
39
+ )
40
+ end
41
+
42
+ def register_constants_referenced_in_class(class_representation)
43
+ constant_nodes = rich_ast.query
44
+ .top_level_constant_references(where: { namespace: class_representation.name })
45
+ .uniq{ |cr| cr.constant_path } # Removes duplicated constants
46
+
47
+ constant_nodes.each do |constant_node|
48
+ data_store = SourceRepresentation::DataStore.instance
49
+ data_store.register_constant(
50
+ constant_node.constant_name,
51
+ constant_node.constant_path[0..-2],
52
+ file_path: file_path,
53
+ caller: class_representation
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,77 @@
1
+ module RubyDetective
2
+ module AST
3
+ class NodeFactory
4
+ attr_reader :node, :rich_node, :file_path, :parent_node
5
+
6
+ # A dictionary that converts the Parser gem type to our Rich AST type
7
+ NODE_TYPE_DICTIONARY = {
8
+ const: :constant,
9
+ class: :class,
10
+ module: :module,
11
+ cbase: :absolute_path_sign
12
+ }
13
+ # The following types also exist:
14
+ #
15
+ # value - the last node of a branch, can be nil, a string, a symbol, etc...
16
+ # generic - a broader "others" type, for any nodes not mapped out
17
+
18
+ def initialize(node, file_path:, parent_node: nil)
19
+ @node = node
20
+ @rich_node = nil
21
+ @file_path = file_path
22
+ @parent_node = parent_node
23
+ end
24
+
25
+ def build
26
+ @rich_node = node_class.new(node, file_path: file_path, parent_node: parent_node)
27
+ end
28
+
29
+ def process_all_children
30
+ rich_node.raw_children.each do |raw_child_node|
31
+ factory = self.class.new(
32
+ raw_child_node,
33
+ file_path: file_path,
34
+ parent_node: rich_node
35
+ )
36
+ child_node = factory.build
37
+
38
+ rich_node.children << child_node
39
+ factory.process_all_children
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def node_class
46
+ case node_type
47
+ when :class
48
+ Nodes::ClassDeclarationNode
49
+ when :module
50
+ Nodes::ModuleDeclarationNode
51
+ when :constant
52
+ Nodes::ConstantReferenceNode
53
+ when :absolute_path_sign
54
+ Nodes::AbsolutePathSignNode
55
+ when :value
56
+ Nodes::ValueNode
57
+ when :generic
58
+ Nodes::GenericNode
59
+ end
60
+ end
61
+
62
+ def node_type
63
+ if not_an_ast_node?
64
+ :value
65
+ elsif NODE_TYPE_DICTIONARY.key?(node.type)
66
+ NODE_TYPE_DICTIONARY[node.type]
67
+ else
68
+ :generic
69
+ end
70
+ end
71
+
72
+ def not_an_ast_node?
73
+ !node.is_a?(Parser::AST::Node)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,11 @@
1
+ module RubyDetective
2
+ module AST
3
+ module Nodes
4
+ class AbsolutePathSignNode < GenericNode
5
+ def type
6
+ :absolute_path_sign
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ module RubyDetective
2
+ module AST
3
+ module Nodes
4
+ class ClassDeclarationNode < GenericNode
5
+ CLASS_NAME_NODE_INDEX = 0
6
+ def class_name
7
+ children[CLASS_NAME_NODE_INDEX].constant_name
8
+ end
9
+
10
+ INHERITANCE_CLASS_NAME_NODE_INDEX = 1
11
+ def inheritance_class_name
12
+ inherited_class_constant = children[INHERITANCE_CLASS_NAME_NODE_INDEX]
13
+ # If this child isn't a ConstantReference node it means it doesn't
14
+ # have a declared class inheritance like "class Foo::Bar"
15
+ return unless inherited_class_constant.constant_reference_node?
16
+
17
+ inherited_class_constant.constant_name
18
+ end
19
+
20
+ PREPENDED_NAMESPACE_NODE_INDEX = 0
21
+ def declared_namespace
22
+ prepended_namespace =
23
+ children[CLASS_NAME_NODE_INDEX]
24
+ .children[PREPENDED_NAMESPACE_NODE_INDEX]
25
+
26
+ return [class_name] unless prepended_namespace.constant_reference_node?
27
+ # This scenario happens when we have a class declaration with inline
28
+ # namespace, like this: class MyNamespace::MyClass
29
+ prepended_namespace.constant_path + [class_name]
30
+ end
31
+
32
+ def type
33
+ :class
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ module RubyDetective
2
+ module AST
3
+ module Nodes
4
+ class ConstantReferenceNode < GenericNode
5
+ CONSTANT_NAME_INDEX = 1
6
+ def constant_name
7
+ children[CONSTANT_NAME_INDEX].value
8
+ end
9
+
10
+ # A top level constant is for example the "Bar" from "Foo::Bar".
11
+ # We access it by checking if the parent is another constant, if it is
12
+ # it means the constant is a nested one and not top level.
13
+ def top_level_constant?
14
+ !parent_node.constant_reference_node?
15
+ end
16
+
17
+ # Recursively builds the constant path by traversing it's children,
18
+ # that way we can compose a path composed of multiple namespaces,
19
+ # for example: Foo::Bar::Batz as [:Foo, :Bar, :Batz].
20
+ NESTED_CONSTANT_INDEX = 0
21
+ def constant_path
22
+ nested_constant = children[NESTED_CONSTANT_INDEX]
23
+
24
+ if nested_constant.constant_reference_node?
25
+ nested_constant.constant_path + [constant_name]
26
+ elsif nested_constant.absolute_path_sign_node?
27
+ # This is used to signify that the constant path was forced to start
28
+ # from the root, for example: "::Foo::Bar"
29
+ [:"::"] + [constant_name]
30
+ else
31
+ [constant_name]
32
+ end
33
+ end
34
+
35
+ def type
36
+ :constant
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end