dependencytree 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +232 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/dependencytree +3 -0
- data/bin/setup +8 -0
- data/dependencytree.gemspec +29 -0
- data/lib/dependencytree.rb +75 -0
- data/lib/dependencytree/classcontainer.rb +116 -0
- data/lib/dependencytree/classmodel.rb +105 -0
- data/lib/dependencytree/dependencyaggregator.rb +192 -0
- data/lib/dependencytree/version.rb +3 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 871b5cca00b7e8c672ebe92d50d82ce45312b676
|
4
|
+
data.tar.gz: 24ba59e668837116dde2ccea7f5fdc5363cc0853
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 02d79005326ff09a232d65027e7e25f6188693a516a82a81a4385d84dd2aa0890de0cdb979794346a3afc8b6e5cf849286092c31f1b913baefb40811e6150fa9
|
7
|
+
data.tar.gz: 58ef9a55ed91519884a254cd7f48c022569e4eb228162e158794398975fc6ed15e0dba89108384e0fa31f339f7dbc650aa0d01deb71c354c3653e3491bc9075c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 1&1 Internet SE
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
# Dependencytree
|
2
|
+
|
3
|
+
Parses ruby source codes and tries to build a dependency tree from the seen classes, modules and references.
|
4
|
+
The output is a JSON file that contains all modules/classes along with their references.
|
5
|
+
|
6
|
+
The references can be used to build a dependency tree for your project.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Install it yourself as from https://rubygems.org/ by calling:
|
11
|
+
|
12
|
+
$ gem install dependencytree
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Use the program in the shell and give it all ruby sources or folders containing ruby sources.
|
17
|
+
|
18
|
+
dependencytree ./lib
|
19
|
+
|
20
|
+
The output will be a JSON of the references. The interesting parts are:
|
21
|
+
* **uuid**: Every class/module has a unique UUID for referencing. The UUID will stay unique only for one parsing run.
|
22
|
+
* **resolved_refs**: Resolved / found references that are pointing to the UUID of the refered class.
|
23
|
+
* **unresolved_refs**: Unresolved references that could not be found inside the sources provided.
|
24
|
+
This can be Ruby classes or other classes from gems that were not scanned.
|
25
|
+
The unresolved_refs entries are arrays of the name parts, for example `[ "moduleA", "mobuleB", "classC" ]`
|
26
|
+
* **methods**: List of seen method names.
|
27
|
+
* **constants**: List of seen constant names.
|
28
|
+
* **path**: Filesystem path of the first source file with a class/module definition.
|
29
|
+
* **type**: `"module"` or `"class"`.
|
30
|
+
* **refs**: An array of all references (resolvable and unresolvable) in array notation (see above).
|
31
|
+
* **name**: The local module/class name, for example `"Stat"`.
|
32
|
+
* **full_name**: The full name of the module/class, for example `"File::Stat"`.
|
33
|
+
|
34
|
+
The following is the example for the dependency tree tool itself:
|
35
|
+
|
36
|
+
```
|
37
|
+
[
|
38
|
+
{
|
39
|
+
"methods" : [],
|
40
|
+
"full_name" : "Kernel",
|
41
|
+
"unresolved_refs" : [],
|
42
|
+
"uuid" : "424ab3a4-60d3-4e7d-9374-8d1d72bd0d91",
|
43
|
+
"type" : "module",
|
44
|
+
"resolved_refs" : [],
|
45
|
+
"refs" : [],
|
46
|
+
"constants" : [],
|
47
|
+
"path" : null,
|
48
|
+
"name" : "Kernel"
|
49
|
+
},
|
50
|
+
{
|
51
|
+
"type" : "module",
|
52
|
+
"uuid" : "401771ee-5c37-4bdf-a10d-f9a467827791",
|
53
|
+
"constants" : [
|
54
|
+
"VERSION"
|
55
|
+
],
|
56
|
+
"name" : "Dependencytree",
|
57
|
+
"path" : "/home/stephan/git/z/dependencytree/lib/dependencytree/classcontainer.rb",
|
58
|
+
"resolved_refs" : [
|
59
|
+
"aad66db1-8229-4565-8a9e-d1ea73d2281c",
|
60
|
+
"1212a5b6-75b8-4836-8867-14a8734bf3e2"
|
61
|
+
],
|
62
|
+
"refs" : [
|
63
|
+
[
|
64
|
+
"File"
|
65
|
+
],
|
66
|
+
[
|
67
|
+
"Dir"
|
68
|
+
],
|
69
|
+
[
|
70
|
+
"Parser",
|
71
|
+
"CurrentRuby"
|
72
|
+
],
|
73
|
+
[
|
74
|
+
"Logger"
|
75
|
+
],
|
76
|
+
[
|
77
|
+
"OptionParser"
|
78
|
+
],
|
79
|
+
[
|
80
|
+
"DependencyAggregator"
|
81
|
+
],
|
82
|
+
[
|
83
|
+
"ARGV"
|
84
|
+
],
|
85
|
+
[
|
86
|
+
"ClassContainer"
|
87
|
+
]
|
88
|
+
],
|
89
|
+
"full_name" : "Dependencytree",
|
90
|
+
"methods" : [],
|
91
|
+
"unresolved_refs" : [
|
92
|
+
[
|
93
|
+
"File"
|
94
|
+
],
|
95
|
+
[
|
96
|
+
"Dir"
|
97
|
+
],
|
98
|
+
[
|
99
|
+
"Parser",
|
100
|
+
"CurrentRuby"
|
101
|
+
],
|
102
|
+
[
|
103
|
+
"Logger"
|
104
|
+
],
|
105
|
+
[
|
106
|
+
"OptionParser"
|
107
|
+
],
|
108
|
+
[
|
109
|
+
"ARGV"
|
110
|
+
]
|
111
|
+
]
|
112
|
+
},
|
113
|
+
{
|
114
|
+
"parent_uuid" : "401771ee-5c37-4bdf-a10d-f9a467827791",
|
115
|
+
"type" : "class",
|
116
|
+
"uuid" : "1212a5b6-75b8-4836-8867-14a8734bf3e2",
|
117
|
+
"constants" : [],
|
118
|
+
"name" : "ClassContainer",
|
119
|
+
"path" : "/home/stephan/git/z/dependencytree/lib/dependencytree/classcontainer.rb",
|
120
|
+
"refs" : [],
|
121
|
+
"resolved_refs" : [],
|
122
|
+
"full_name" : "Dependencytree::ClassContainer",
|
123
|
+
"methods" : [
|
124
|
+
"initialize",
|
125
|
+
"resolve_references",
|
126
|
+
"resolve_reference",
|
127
|
+
"resolve_reference_as_constant",
|
128
|
+
"resolve_reference_direct",
|
129
|
+
"resolve_by_full_name",
|
130
|
+
"resolve_by_name"
|
131
|
+
],
|
132
|
+
"unresolved_refs" : []
|
133
|
+
},
|
134
|
+
{
|
135
|
+
"full_name" : "Dependencytree::ClassModel",
|
136
|
+
"methods" : [
|
137
|
+
"initialize",
|
138
|
+
"full_name_array",
|
139
|
+
"full_name",
|
140
|
+
"set_parent",
|
141
|
+
"as_json",
|
142
|
+
"to_json",
|
143
|
+
"add_method",
|
144
|
+
"add_constant",
|
145
|
+
"add_reference"
|
146
|
+
],
|
147
|
+
"unresolved_refs" : [
|
148
|
+
[
|
149
|
+
"SecureRandom"
|
150
|
+
],
|
151
|
+
[
|
152
|
+
"ArgumentError"
|
153
|
+
]
|
154
|
+
],
|
155
|
+
"uuid" : "9a267b3e-0784-4114-810f-8d38484dbbc7",
|
156
|
+
"type" : "class",
|
157
|
+
"parent_uuid" : "401771ee-5c37-4bdf-a10d-f9a467827791",
|
158
|
+
"refs" : [
|
159
|
+
[
|
160
|
+
"SecureRandom"
|
161
|
+
],
|
162
|
+
[
|
163
|
+
"ArgumentError"
|
164
|
+
]
|
165
|
+
],
|
166
|
+
"resolved_refs" : [],
|
167
|
+
"path" : "/home/stephan/git/z/dependencytree/lib/dependencytree/classmodel.rb",
|
168
|
+
"constants" : [],
|
169
|
+
"name" : "ClassModel"
|
170
|
+
},
|
171
|
+
{
|
172
|
+
"unresolved_refs" : [
|
173
|
+
[
|
174
|
+
"ArgumentError"
|
175
|
+
],
|
176
|
+
[
|
177
|
+
"Exception"
|
178
|
+
]
|
179
|
+
],
|
180
|
+
"methods" : [
|
181
|
+
"initialize",
|
182
|
+
"top_of_stack",
|
183
|
+
"flatten_const_tree",
|
184
|
+
"_resolve",
|
185
|
+
"_const",
|
186
|
+
"_module",
|
187
|
+
"_class",
|
188
|
+
"_handle_class_module_common",
|
189
|
+
"_def",
|
190
|
+
"_casgn",
|
191
|
+
"visit_node",
|
192
|
+
"visit_children",
|
193
|
+
"visit"
|
194
|
+
],
|
195
|
+
"full_name" : "Dependencytree::DependencyAggregator",
|
196
|
+
"path" : "/home/stephan/git/z/dependencytree/lib/dependencytree/dependencyaggregator.rb",
|
197
|
+
"constants" : [],
|
198
|
+
"name" : "DependencyAggregator",
|
199
|
+
"refs" : [
|
200
|
+
[
|
201
|
+
"ArgumentError"
|
202
|
+
],
|
203
|
+
[
|
204
|
+
"ClassModel"
|
205
|
+
],
|
206
|
+
[
|
207
|
+
"Exception"
|
208
|
+
]
|
209
|
+
],
|
210
|
+
"resolved_refs" : [
|
211
|
+
"9a267b3e-0784-4114-810f-8d38484dbbc7"
|
212
|
+
],
|
213
|
+
"parent_uuid" : "401771ee-5c37-4bdf-a10d-f9a467827791",
|
214
|
+
"type" : "class",
|
215
|
+
"uuid" : "aad66db1-8229-4565-8a9e-d1ea73d2281c"
|
216
|
+
}
|
217
|
+
]
|
218
|
+
```
|
219
|
+
|
220
|
+
## Development
|
221
|
+
|
222
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
223
|
+
|
224
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
225
|
+
|
226
|
+
## Contributing
|
227
|
+
|
228
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/1and1/dependencytree.
|
229
|
+
|
230
|
+
## License
|
231
|
+
|
232
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "dependencytree"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/dependencytree
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "dependencytree/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dependencytree"
|
8
|
+
spec.version = Dependencytree::VERSION
|
9
|
+
spec.authors = ["Stephan Fuhrmann"]
|
10
|
+
spec.email = ["stephan.fuhrmann@1und1.de"]
|
11
|
+
|
12
|
+
spec.summary = %q{Calculate class dependencies and output to JSON.}
|
13
|
+
spec.description = %q{Analyse Ruby source code, analyse class dependencies and output everything as a JSON.}
|
14
|
+
spec.homepage = "http://www.1und1.de"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "bin"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.executables << 'dependencytree'
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_dependency "parser", "~> 2.5.0.5"
|
28
|
+
spec.add_dependency "ast", "~> 2.4.0"
|
29
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "dependencytree/dependencyaggregator"
|
2
|
+
require "dependencytree/classcontainer"
|
3
|
+
|
4
|
+
require "dependencytree/version"
|
5
|
+
require 'parser/current'
|
6
|
+
require 'optparse'
|
7
|
+
require 'pp'
|
8
|
+
require 'json'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
module Dependencytree
|
12
|
+
|
13
|
+
def self.handle_path(options, consumer, path)
|
14
|
+
if options[:ignore].match(path)
|
15
|
+
return
|
16
|
+
end
|
17
|
+
if File.directory?(path)
|
18
|
+
Dir.entries(path).each { |x|
|
19
|
+
resolved = File.join(path, x)
|
20
|
+
handle_path(options, consumer, resolved) if File.directory?(resolved) && x != "." && x != ".."
|
21
|
+
handle_path(options, consumer, resolved) if File.file?(resolved) && options[:pattern].match(resolved)
|
22
|
+
}
|
23
|
+
elsif File.file?(path)
|
24
|
+
$LOG.debug("Handling path #{path}")
|
25
|
+
tree = Parser::CurrentRuby.parse_file(path)
|
26
|
+
$LOG.debug("Parsed tree: #{tree}") if $LOG.debug?
|
27
|
+
consumer.visit(path, tree)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
$LOG = Logger.new('application.log', 'daily', 20)
|
32
|
+
|
33
|
+
options = {}
|
34
|
+
options[:ignore] = /^$/
|
35
|
+
options[:pattern] = /.*\.rb/
|
36
|
+
OptionParser.new do |opt|
|
37
|
+
opt.on("-v", "--verbose", "Verbose output") do |o|
|
38
|
+
# TBD
|
39
|
+
options[:verbose] = true
|
40
|
+
$LOG.debug("verbose")
|
41
|
+
end
|
42
|
+
opt.on("-p", "--pattern[=OPTIONAL]", "Pattern to accept source codes with (default: #{options[:pattern].to_s})") do |o|
|
43
|
+
options[:pattern] = /#{o}/
|
44
|
+
$LOG.debug("pattern = #{o}")
|
45
|
+
end
|
46
|
+
opt.on("-i", "--ignore[=OPTIONAL]", "Paths to not load (default: #{options[:ignore].to_s})") do |o|
|
47
|
+
options[:ignore] = /#{o}/
|
48
|
+
$LOG.debug("ignore = #{o}")
|
49
|
+
end
|
50
|
+
opt.on("-o", "--output[=OPTIONAL]", "Output path for the JSON file") do |o|
|
51
|
+
options[:output] = o
|
52
|
+
$LOG.debug("output = #{o}")
|
53
|
+
end
|
54
|
+
opt.on_tail("-h", "--help", "Show this message") do
|
55
|
+
puts opt
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
end.parse!
|
59
|
+
|
60
|
+
consumer = DependencyAggregator.new
|
61
|
+
ARGV.each do |path|
|
62
|
+
handle_path(options, consumer, File.absolute_path(path))
|
63
|
+
end
|
64
|
+
|
65
|
+
classcontainer = ClassContainer.new(consumer.classes_and_modules)
|
66
|
+
classcontainer.resolve_references
|
67
|
+
|
68
|
+
json = classcontainer.classes_and_modules.to_json
|
69
|
+
if options[:output]
|
70
|
+
File.write(options[:output], json)
|
71
|
+
else
|
72
|
+
puts json
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require "dependencytree/version"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
module Dependencytree
|
5
|
+
|
6
|
+
# Model for classes and modules.
|
7
|
+
class ClassContainer
|
8
|
+
|
9
|
+
attr_reader :classes_and_modules
|
10
|
+
|
11
|
+
# type: :class or :module
|
12
|
+
# path: the filesystem path the parsed class was found in
|
13
|
+
# module_name: eventual module name or :anonymous
|
14
|
+
# class_name: the class name
|
15
|
+
def initialize(classes_and_modules)
|
16
|
+
@classes_and_modules = classes_and_modules
|
17
|
+
@by_full_name = @classes_and_modules.each_with_object({}) { |clazz, hash| hash[clazz.full_name()] = clazz }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Goes thru all classes and tries to resolve the references.
|
21
|
+
def resolve_references
|
22
|
+
@classes_and_modules.each do |clazz|
|
23
|
+
$LOG.debug("Processing class #{clazz.full_name} located in #{clazz.path}")
|
24
|
+
clazz.references.each do |reference_array|
|
25
|
+
refered_class = resolve_reference(clazz, reference_array)
|
26
|
+
if refered_class
|
27
|
+
clazz.resolved_references << refered_class.uuid
|
28
|
+
else
|
29
|
+
clazz.unresolved_references << reference_array
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Tries to resolve one reference either as a constant or a class/module reference.
|
36
|
+
# @param referer_class_model the ClassModel where the reference is in.
|
37
|
+
# @param reference_array the reference as in the source, can be absolute or relative to the referer class.
|
38
|
+
# @return the ClassModel refering to (also for constants!).
|
39
|
+
def resolve_reference(referer_class_model, reference_array)
|
40
|
+
refered_class_model = resolve_reference_direct(referer_class_model, reference_array)
|
41
|
+
if ! refered_class_model
|
42
|
+
refered_class_model = resolve_reference_as_constant(referer_class_model, reference_array)
|
43
|
+
end
|
44
|
+
|
45
|
+
if refered_class_model
|
46
|
+
$LOG.debug("Resolved #{reference_array.join('::')} to uuid #{refered_class_model.uuid}")
|
47
|
+
else
|
48
|
+
$LOG.debug("Could not resolve #{reference_array.join('::')}")
|
49
|
+
end
|
50
|
+
refered_class_model
|
51
|
+
end
|
52
|
+
|
53
|
+
# Resolve a constant module/class reference.
|
54
|
+
# @param referer_class_model the ClassModel where the reference is in.
|
55
|
+
# @param reference_array the reference as in the source, can be absolute or relative to the referer class. The last
|
56
|
+
# element of the array is the constant name.
|
57
|
+
# @return the refered class model or nil
|
58
|
+
def resolve_reference_as_constant(referer_class_model, reference_array)
|
59
|
+
reference_part = reference_array[0..-2]
|
60
|
+
constant_name = reference_array[-1]
|
61
|
+
|
62
|
+
$LOG.debug("Resolving reference array #{reference_array.to_s} as reference #{reference_part.to_s} and constant #{constant_name}")
|
63
|
+
|
64
|
+
refered_class_model = resolve_reference_direct(referer_class_model, reference_part)
|
65
|
+
if refered_class_model
|
66
|
+
$LOG.debug("Found reference to possible parent #{reference_part.to_s}")
|
67
|
+
if refered_class_model.constant_names.include? constant_name.to_sym
|
68
|
+
$LOG.debug("Found class #{refered_class_model.full_name} constant #{constant_name}")
|
69
|
+
refered_class_model
|
70
|
+
else
|
71
|
+
$LOG.debug("Found class #{refered_class_model.full_name}, but not constant #{constant_name}. Known constants: #{refered_class_model.constant_names}")
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
else
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Resolve a full module/class reference.
|
80
|
+
# @param referer_class_model the ClassModel where the reference is in.
|
81
|
+
# @param reference_array the reference as in the source, can be absolute or relative to the referer class.
|
82
|
+
# @return the refered class model or nil
|
83
|
+
def resolve_reference_direct(referer_class_model, reference_array)
|
84
|
+
$LOG.debug("Resolving reference array #{reference_array.to_s}")
|
85
|
+
|
86
|
+
referer_array = referer_class_model.full_name_array
|
87
|
+
i = 0
|
88
|
+
refered_class_model = nil
|
89
|
+
while !refered_class_model do
|
90
|
+
$LOG.debug("Referer array #{i} is #{referer_array.to_s}")
|
91
|
+
full_name = (referer_array+reference_array).join("::")
|
92
|
+
$LOG.debug("Full name #{i} is #{full_name} #{full_name.class.name}")
|
93
|
+
refered_class_model = resolve_by_full_name(full_name)
|
94
|
+
|
95
|
+
break if referer_array.empty?
|
96
|
+
referer_array = referer_array[0..-2]
|
97
|
+
i += 1
|
98
|
+
end
|
99
|
+
|
100
|
+
refered_class_model
|
101
|
+
end
|
102
|
+
|
103
|
+
# Finds a class or module by its full name.
|
104
|
+
# @param full_name the full name to search for.
|
105
|
+
def resolve_by_full_name(full_name)
|
106
|
+
@by_full_name[full_name]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Finds a class or module by its local name.
|
110
|
+
# @param name the name to search for.
|
111
|
+
def resolve_by_name(name)
|
112
|
+
@classes_and_modules.find { |clazz| clazz.name == name }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "dependencytree/version"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
module Dependencytree
|
5
|
+
|
6
|
+
# Model for classes and modules
|
7
|
+
class ClassModel
|
8
|
+
|
9
|
+
attr_reader :uuid
|
10
|
+
attr_reader :name
|
11
|
+
attr_reader :references
|
12
|
+
attr_reader :path
|
13
|
+
attr_reader :constant_names
|
14
|
+
attr_reader :method_names
|
15
|
+
|
16
|
+
attr_reader :resolved_references
|
17
|
+
attr_reader :unresolved_references
|
18
|
+
|
19
|
+
# type: :class or :module
|
20
|
+
# path: the filesystem path the parsed class was found in
|
21
|
+
# module_name: eventual module name or :anonymous
|
22
|
+
# class_name: the class name
|
23
|
+
def initialize(type, path, name)
|
24
|
+
# unique uuid for reference
|
25
|
+
@uuid = SecureRandom.uuid
|
26
|
+
# :module or :class
|
27
|
+
@type = type
|
28
|
+
# filesystem path of (first) definition
|
29
|
+
@path = path
|
30
|
+
# local name (without enclosing modules)
|
31
|
+
@name = name
|
32
|
+
# list of names of methods
|
33
|
+
@method_names = []
|
34
|
+
# list of names of constants
|
35
|
+
@constant_names = []
|
36
|
+
# list of (unresolved) references as arrays
|
37
|
+
@references = []
|
38
|
+
|
39
|
+
@resolved_references = []
|
40
|
+
@unresolved_references = []
|
41
|
+
end
|
42
|
+
|
43
|
+
# Gets the full name of the class/module as an array.
|
44
|
+
# @return the full name, for example ["ModuleA","ModuleB","ClassA"]
|
45
|
+
def full_name_array
|
46
|
+
if @parent
|
47
|
+
result = @parent.full_name_array << @name.to_s
|
48
|
+
else
|
49
|
+
result = [ @name.to_s ]
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
# Gets the full name of the class/module.
|
55
|
+
# @return the full name, for example "ModuleA::ModuleB::ClassA"
|
56
|
+
def full_name
|
57
|
+
full_name_array.join("::")
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_parent(parent)
|
61
|
+
raise ArgumentError, "Self parent reference for name #{@name}" if parent == self
|
62
|
+
@parent = parent
|
63
|
+
end
|
64
|
+
|
65
|
+
def as_json(*a)
|
66
|
+
result = {
|
67
|
+
"uuid" => @uuid,
|
68
|
+
"type" => @type,
|
69
|
+
"path" => @path,
|
70
|
+
"name" => @name,
|
71
|
+
"full_name" => full_name,
|
72
|
+
"methods" => @method_names,
|
73
|
+
"constants" => @constant_names,
|
74
|
+
"refs" => @references.uniq,
|
75
|
+
"resolved_refs" => @resolved_references.uniq,
|
76
|
+
"unresolved_refs" => @unresolved_references.uniq,
|
77
|
+
}
|
78
|
+
|
79
|
+
if @parent
|
80
|
+
result["parent_uuid"] = @parent.uuid
|
81
|
+
end
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_json(*a)
|
86
|
+
as_json.to_json(*a)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Adds a method by its name to the list of methods.
|
90
|
+
def add_method(method_name)
|
91
|
+
@method_names << method_name.to_sym
|
92
|
+
end
|
93
|
+
|
94
|
+
# Adds a constant by its name to the list of constants.
|
95
|
+
def add_constant(constant_name)
|
96
|
+
@constant_names << constant_name.to_sym
|
97
|
+
end
|
98
|
+
|
99
|
+
# Adds a reference by its array-style full name.
|
100
|
+
def add_reference(ref)
|
101
|
+
@references << ref
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require "dependencytree/version"
|
2
|
+
require 'parser/current'
|
3
|
+
require "dependencytree/classmodel"
|
4
|
+
|
5
|
+
module Dependencytree
|
6
|
+
class DependencyAggregator
|
7
|
+
attr_reader :classes_and_modules
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
# path will be the file system path of the source file
|
11
|
+
@path = nil
|
12
|
+
# context_stack is the stack of modules/classes loaded (namespacing)
|
13
|
+
@context_stack = []
|
14
|
+
# this is a flat list of all classes / modules seen
|
15
|
+
@classes_and_modules = []
|
16
|
+
|
17
|
+
# force adding the Kernel module to the list of classes
|
18
|
+
_handle_class_module_common(:module, "Kernel", nil)
|
19
|
+
@kernel = _resolve("Kernel")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Gets the top of stack class/module or Kernel if nothing set.
|
23
|
+
def top_of_stack
|
24
|
+
if @context_stack.empty?
|
25
|
+
@kernel
|
26
|
+
else
|
27
|
+
@context_stack[-1]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Make an array of strings out of a encapsulated tree of :const expressions.
|
32
|
+
# @param node the top const node to start flattening at.
|
33
|
+
def flatten_const_tree(node)
|
34
|
+
raise ArgumentError, "type needs to be const (#{node.type})" if node.type != :const
|
35
|
+
raise ArgumentError, "Children count needs to be 2 (#{node.children.length})" if node.children.length != 2
|
36
|
+
|
37
|
+
result = node.children[1..1]
|
38
|
+
if node.children[0] && node.children[0].type == :const
|
39
|
+
result = flatten_const_tree(node.children[0]) + result
|
40
|
+
end
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
# Finds a class or module by its full name.
|
45
|
+
# @param full_name the full name.
|
46
|
+
def _resolve(full_name)
|
47
|
+
@classes_and_modules.find { |clazz| clazz.full_name.to_s == full_name.to_s }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Handle a const expression.
|
51
|
+
# @param node the const node itself to handle.
|
52
|
+
def _const(node)
|
53
|
+
$LOG.debug("const")
|
54
|
+
|
55
|
+
raise ArgumentError, "type needs to be const (#{node.type})" if node.type != :const
|
56
|
+
raise ArgumentError, "Children count needs to be 2 (#{node.children.length})" if node.children.length != 2
|
57
|
+
|
58
|
+
reference = flatten_const_tree(node)
|
59
|
+
|
60
|
+
$LOG.debug("Reference to #{reference.to_s}")
|
61
|
+
top_of_stack.add_reference(reference)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handle a module expression.
|
65
|
+
# @param node the module node itself to handle.
|
66
|
+
def _module(node)
|
67
|
+
raise ArgumentError, "Children count for module is != 2 (#{node.children.length})" if node.children.length != 2
|
68
|
+
raise ArgumentError, "First module child needs to be a const (#{node.children[0].type} #{node.children[0].type})" if node.children[0].type != :const
|
69
|
+
|
70
|
+
$LOG.debug("module #{node.children[0].children[1]}")
|
71
|
+
|
72
|
+
current_module_name = node.children[0].children[1]
|
73
|
+
_handle_class_module_common(:module, current_module_name, node)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Handle a class expression.
|
77
|
+
# @param node the class node itself to handle.
|
78
|
+
def _class(node)
|
79
|
+
raise ArgumentError, "Children count for class is != 3 (#{node.children.length})" if node.children.length != 3
|
80
|
+
raise ArgumentError, "First class child needs to be a const (#{node.children[0].type} #{node.children[0].type})" if node.children[0].type != :const
|
81
|
+
$LOG.debug("class #{node.children[0].children[1]}")
|
82
|
+
|
83
|
+
current_class_name = node.children[0].children[1]
|
84
|
+
_handle_class_module_common(:class, current_class_name, node)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Handle the common parts of a module or class definition. Will try to resolve the instance or create it if not found.
|
88
|
+
# @param type :module or :class.
|
89
|
+
# @param name the local class name.
|
90
|
+
# @param node the AST node of the class or module, can be nil if no children traversal required.
|
91
|
+
def _handle_class_module_common(type, name, node)
|
92
|
+
full_name = name
|
93
|
+
parent = nil
|
94
|
+
if ! @context_stack.empty?
|
95
|
+
parent = @context_stack[-1]
|
96
|
+
full_name = parent.full_name.to_s + "::" + name.to_s
|
97
|
+
end
|
98
|
+
$LOG.debug("Full name is #{full_name}")
|
99
|
+
resolved = _resolve(full_name)
|
100
|
+
|
101
|
+
if ! resolved.nil?
|
102
|
+
# found an existing module/class with the full_name
|
103
|
+
model = resolved
|
104
|
+
else
|
105
|
+
# found no existing module/class with the full_name
|
106
|
+
model = ClassModel.new(type, @path, name)
|
107
|
+
if parent
|
108
|
+
model.set_parent(parent)
|
109
|
+
end
|
110
|
+
@classes_and_modules << model
|
111
|
+
end
|
112
|
+
|
113
|
+
if resolved.nil?
|
114
|
+
$LOG.debug("Created new ClassModel for #{model.full_name}")
|
115
|
+
end
|
116
|
+
|
117
|
+
@context_stack << model
|
118
|
+
# recurse over the contents of the module
|
119
|
+
visit_children(node.children[1..-1]) if node
|
120
|
+
@context_stack.pop
|
121
|
+
end
|
122
|
+
|
123
|
+
# Handle a def expression.
|
124
|
+
# @param node the def node itself to handle.
|
125
|
+
def _def(node)
|
126
|
+
raise ArgumentError, "Children count for def is != 3 (#{node.children.length})" if node.children.length != 3
|
127
|
+
|
128
|
+
$LOG.debug("def #{node.children[0]}")
|
129
|
+
|
130
|
+
top_of_stack.add_method(node.children[0])
|
131
|
+
|
132
|
+
visit_children(node.children[1..-1])
|
133
|
+
end
|
134
|
+
|
135
|
+
# Handle a def expression.
|
136
|
+
# @param node the def node itself to handle.
|
137
|
+
def _casgn(node)
|
138
|
+
raise ArgumentError, "Children count for casgn is != 3 (#{node.children.length})" if node.children.length != 3
|
139
|
+
|
140
|
+
$LOG.debug("casgn #{node.children[1]}")
|
141
|
+
|
142
|
+
top_of_stack.add_constant(node.children[1])
|
143
|
+
|
144
|
+
visit_children(node.children[1..-1])
|
145
|
+
end
|
146
|
+
|
147
|
+
# Visit a AST node and do the appropriate actions.
|
148
|
+
# @param node the node to visit.
|
149
|
+
def visit_node(node)
|
150
|
+
case node.type
|
151
|
+
when :const
|
152
|
+
_const(node)
|
153
|
+
when :class
|
154
|
+
_class(node)
|
155
|
+
when :module
|
156
|
+
_module(node)
|
157
|
+
when :def
|
158
|
+
_def(node)
|
159
|
+
when :casgn
|
160
|
+
_casgn(node)
|
161
|
+
else
|
162
|
+
visit_children(node.children)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Visit all children of a node. Will call #visit_node on each node child.
|
167
|
+
# @param children the array of children to visit.
|
168
|
+
def visit_children(children)
|
169
|
+
return if ! children
|
170
|
+
children.each do |child|
|
171
|
+
visit_node(child) if child.respond_to?(:children)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Visits all children of the AST tree.
|
176
|
+
# @param path the filesystem path of the parsed entity (ruby file).
|
177
|
+
# @param tree the AST tree node.
|
178
|
+
def visit(path, tree)
|
179
|
+
begin
|
180
|
+
$LOG.debug("Visiting path #{path}")
|
181
|
+
@path = path
|
182
|
+
visit_node(tree)
|
183
|
+
@path = nil
|
184
|
+
rescue Exception => e
|
185
|
+
$LOG.error("Error in path #{path}")
|
186
|
+
puts "Error in path #{path}"
|
187
|
+
raise e
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dependencytree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephan Fuhrmann
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: parser
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.5.0.5
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.5.0.5
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ast
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.4.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.4.0
|
69
|
+
description: Analyse Ruby source code, analyse class dependencies and output everything
|
70
|
+
as a JSON.
|
71
|
+
email:
|
72
|
+
- stephan.fuhrmann@1und1.de
|
73
|
+
executables:
|
74
|
+
- dependencytree
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/console
|
84
|
+
- bin/dependencytree
|
85
|
+
- bin/setup
|
86
|
+
- dependencytree.gemspec
|
87
|
+
- lib/dependencytree.rb
|
88
|
+
- lib/dependencytree/classcontainer.rb
|
89
|
+
- lib/dependencytree/classmodel.rb
|
90
|
+
- lib/dependencytree/dependencyaggregator.rb
|
91
|
+
- lib/dependencytree/version.rb
|
92
|
+
homepage: http://www.1und1.de
|
93
|
+
licenses:
|
94
|
+
- MIT
|
95
|
+
metadata: {}
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.5.1
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Calculate class dependencies and output to JSON.
|
116
|
+
test_files: []
|