dependencytree 0.1.3
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 +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: []
|