dependencytree 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *~
11
+ *.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dependencytree.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
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__)
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dependencytree'
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
+
@@ -0,0 +1,3 @@
1
+ module Dependencytree
2
+ VERSION = "0.1.3"
3
+ end
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: []