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.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: abe0950ab9ca4d1ed6e201099393deba959859cea88d1d2ca3467dacc38b00d9
|
4
|
+
data.tar.gz: ea9b99edf95de63499ae29a537d2031e312b2c0b7fa7e37947287dd67a96de3c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25e3f035527b125ee423a7b6689889681c935a72c2efe5fe8459cdc649a3050a499421dd005e8f9747b7283a048eda6ff43ca8bfea9439ad5040bfc8935eeb11
|
7
|
+
data.tar.gz: f7b9bb40ec643c51c8e4e2d20bf330a2c2b8b68428c03907f85fffa05cd0ddc46057b866e31510492cdffe1d13fff20acf5787639229b94d7008bafe14b307b2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
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
data/bin/.keep
ADDED
File without changes
|
data/bin/ruby_detective
ADDED
data/lib/.keep
ADDED
File without changes
|
data/lib/ruby_detective.rb
CHANGED
@@ -3,30 +3,26 @@ end
|
|
3
3
|
|
4
4
|
require "parser/current"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
require "ruby_detective/runner"
|
7
|
+
require "ruby_detective/json_builder"
|
8
|
+
require "ruby_detective/ui_generator"
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
18
|
+
require "ruby_detective/ast/file_parser"
|
19
|
+
require "ruby_detective/ast/interpreter"
|
20
|
+
require "ruby_detective/ast/node_factory"
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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,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
|