analyst 0.0.1 → 0.13.1
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 +4 -4
- data/.gitignore +4 -1
- data/.rspec +2 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/README.md +1 -0
- data/analyst.gemspec +12 -0
- data/lib/analyst/analyzer.rb +162 -0
- data/lib/analyst/cli.rb +42 -0
- data/lib/analyst/entity_parser/association.rb +24 -0
- data/lib/analyst/entity_parser/entities/class.rb +92 -0
- data/lib/analyst/entity_parser/entities/empty.rb +13 -0
- data/lib/analyst/entity_parser/entities/entity.rb +29 -0
- data/lib/analyst/entity_parser/entities/method.rb +16 -0
- data/lib/analyst/entity_parser/entities/module.rb +31 -0
- data/lib/analyst/formatters/base.rb +33 -0
- data/lib/analyst/formatters/csv.rb +43 -0
- data/lib/analyst/formatters/html.rb +87 -0
- data/lib/analyst/formatters/html_index.rb +47 -0
- data/lib/analyst/formatters/templates/index.html.haml +92 -0
- data/lib/analyst/formatters/templates/output.html.haml +114 -0
- data/lib/analyst/formatters/text.rb +56 -0
- data/lib/analyst/fukuzatsu/analyzer.rb +162 -0
- data/lib/analyst/fukuzatsu/cli.rb +42 -0
- data/lib/analyst/fukuzatsu/entity_parser/association.rb +24 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/class.rb +92 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/empty.rb +13 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/entity.rb +29 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/method.rb +16 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/module.rb +31 -0
- data/lib/analyst/fukuzatsu/formatters/base.rb +33 -0
- data/lib/analyst/fukuzatsu/formatters/csv.rb +43 -0
- data/lib/analyst/fukuzatsu/formatters/html.rb +87 -0
- data/lib/analyst/fukuzatsu/formatters/html_index.rb +47 -0
- data/lib/analyst/fukuzatsu/formatters/templates/index.html.haml +92 -0
- data/lib/analyst/fukuzatsu/formatters/templates/output.html.haml +114 -0
- data/lib/analyst/fukuzatsu/formatters/text.rb +56 -0
- data/lib/analyst/fukuzatsu/line_of_code.rb +19 -0
- data/lib/analyst/fukuzatsu/parsed_file.rb +85 -0
- data/lib/analyst/fukuzatsu/parsed_method.rb +32 -0
- data/lib/analyst/fukuzatsu/parser_original.rb +76 -0
- data/lib/analyst/fukuzatsu/rethink/parser.rb +346 -0
- data/lib/analyst/fukuzatsu/version.rb +3 -0
- data/lib/analyst/line_of_code.rb +19 -0
- data/lib/analyst/parsed_file.rb +85 -0
- data/lib/analyst/parsed_method.rb +32 -0
- data/lib/analyst/parser.rb +76 -0
- data/lib/analyst/rethink/parser.rb +346 -0
- data/lib/analyst/version.rb +1 -1
- data/lib/analyst.rb +17 -2
- data/spec/analyzer_spec.rb +122 -0
- data/spec/cli_spec.rb +48 -0
- data/spec/fixtures/eg_class.rb +8 -0
- data/spec/fixtures/eg_mod_class.rb +2 -0
- data/spec/fixtures/eg_mod_class_2.rb +5 -0
- data/spec/fixtures/eg_module.rb +2 -0
- data/spec/fixtures/module_with_class.rb +9 -0
- data/spec/fixtures/multiple_methods.rb +7 -0
- data/spec/fixtures/nested_methods.rb +8 -0
- data/spec/fixtures/program_1.rb +19 -0
- data/spec/fixtures/program_2.rb +25 -0
- data/spec/fixtures/program_3.rb +66 -0
- data/spec/fixtures/program_4.rb +1 -0
- data/spec/fixtures/single_class.rb +9 -0
- data/spec/fixtures/single_method.rb +3 -0
- data/spec/formatters/csv_spec.rb +37 -0
- data/spec/formatters/html_index_spec.rb +36 -0
- data/spec/formatters/html_spec.rb +48 -0
- data/spec/formatters/text_spec.rb +39 -0
- data/spec/parsed_file_spec.rb +67 -0
- data/spec/parsed_method_spec.rb +34 -0
- data/spec/spec_helper.rb +7 -0
- metadata +229 -2
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Analyst
|
4
|
+
|
5
|
+
class Parser
|
6
|
+
|
7
|
+
attr_reader :start_path, :parsed_files
|
8
|
+
attr_reader :threshold, :formatter
|
9
|
+
attr_reader :start_time
|
10
|
+
|
11
|
+
OUTPUT_DIRECTORY = "doc/Analyst"
|
12
|
+
|
13
|
+
def initialize(path, formatter, threshold=0)
|
14
|
+
@start_path = path
|
15
|
+
@formatter = formatter
|
16
|
+
@threshold = threshold
|
17
|
+
@start_time = Time.now
|
18
|
+
reset_output_directory
|
19
|
+
end
|
20
|
+
|
21
|
+
def parsed_files
|
22
|
+
@parsed_files = source_files.map do |path_to_file|
|
23
|
+
parse_source_file(path_to_file)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def report
|
28
|
+
self.parsed_files.each do |file|
|
29
|
+
print "."
|
30
|
+
formatter.new(file, OUTPUT_DIRECTORY, file.source).export
|
31
|
+
end
|
32
|
+
puts
|
33
|
+
write_report_index
|
34
|
+
report_complexity
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def reset_output_directory
|
40
|
+
begin
|
41
|
+
FileUtils.remove_dir(OUTPUT_DIRECTORY)
|
42
|
+
rescue Errno::ENOENT
|
43
|
+
end
|
44
|
+
FileUtils.mkpath(OUTPUT_DIRECTORY)
|
45
|
+
end
|
46
|
+
|
47
|
+
def source_files
|
48
|
+
if File.directory?(start_path)
|
49
|
+
return Dir.glob(File.join(start_path, "**", "*.rb"))
|
50
|
+
else
|
51
|
+
return [start_path]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_source_file(path_to_file, options={})
|
56
|
+
ParsedFile.new(path_to_file: path_to_file)
|
57
|
+
end
|
58
|
+
|
59
|
+
def report_complexity
|
60
|
+
return if self.threshold == 0
|
61
|
+
complexities = self.parsed_files.map(&:complexity)
|
62
|
+
return if complexities.max.to_i <= self.threshold
|
63
|
+
puts "Maximum complexity of #{complexities.max} exceeds #{options['threshold']} threshold!"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
def write_report_index
|
68
|
+
return unless self.formatter.writes_to_file_system?
|
69
|
+
puts "Results written to #{OUTPUT_DIRECTORY} "
|
70
|
+
return unless self.formatter.has_index?
|
71
|
+
formatter.index_class.new(parsed_files.map(&:summary), OUTPUT_DIRECTORY).export
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
require 'pry'
|
2
|
+
|
3
|
+
module Z
|
4
|
+
|
5
|
+
class Parser
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :corpus
|
9
|
+
|
10
|
+
def_delegators :@corpus, :classes, :associations
|
11
|
+
|
12
|
+
def initialize(path)
|
13
|
+
@corpus = Corpus.new(path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def class_graph
|
17
|
+
{
|
18
|
+
nodes: corpus.classes,
|
19
|
+
edges: corpus.associations
|
20
|
+
#nodes: [1,2,3],
|
21
|
+
#edges: [{source: 1, target: 2, strength: 1}, {source: 2, target: 1, strength: 4}]
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
class Corpus
|
28
|
+
|
29
|
+
attr_reader :path, :classes, :associations
|
30
|
+
|
31
|
+
def initialize(path)
|
32
|
+
@path = path
|
33
|
+
parse!
|
34
|
+
end
|
35
|
+
|
36
|
+
def files
|
37
|
+
@files ||= file_paths.map {|path| File.new(path)}
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse!
|
41
|
+
@classes = extract_classes
|
42
|
+
@associations = link_associations
|
43
|
+
#@associations = combine_identical_associations
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def extract_classes
|
49
|
+
classes = []
|
50
|
+
files.each {|f| f.classes.each {|c| merge_class(classes, c) } }
|
51
|
+
classes
|
52
|
+
end
|
53
|
+
|
54
|
+
def link_associations
|
55
|
+
all = classes.map(&:associations).flatten
|
56
|
+
all.each do |assoc|
|
57
|
+
target = classes.detect {|c| c.full_name == assoc.target_class }
|
58
|
+
if target
|
59
|
+
assoc.target = target
|
60
|
+
else
|
61
|
+
puts "WARNING: Couldn't find target: #{assoc.target_class}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def file_paths
|
67
|
+
if ::File.directory?(path)
|
68
|
+
Dir.glob(::File.join(path, "**", "*.rb"))
|
69
|
+
else
|
70
|
+
[path]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def merge_class(classes, klass)
|
75
|
+
existing = classes.find {|c| c.full_name == klass.full_name}
|
76
|
+
if existing
|
77
|
+
existing.merge(klass)
|
78
|
+
else
|
79
|
+
classes << klass
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
class File
|
87
|
+
|
88
|
+
attr_reader :path
|
89
|
+
|
90
|
+
def initialize(path)
|
91
|
+
@path = path
|
92
|
+
end
|
93
|
+
|
94
|
+
def contents
|
95
|
+
@contents ||= ::File.open(path, 'r').read
|
96
|
+
end
|
97
|
+
|
98
|
+
def ast
|
99
|
+
@ast ||= ::Parser::CurrentRuby.parse(contents)
|
100
|
+
end
|
101
|
+
|
102
|
+
def classes
|
103
|
+
return @classes if @classes
|
104
|
+
@classes = []
|
105
|
+
parse!
|
106
|
+
@classes
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse!
|
110
|
+
parse_ast
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# to log node types that haven't been dealt with
|
117
|
+
def unhandled_node_types
|
118
|
+
@unhandled_node_types ||= Set.new
|
119
|
+
end
|
120
|
+
|
121
|
+
# contains Module, Class, Method... eventually Block and other stuff maybe
|
122
|
+
def context_stack
|
123
|
+
@context_stack ||= [Entities::Empty.new]
|
124
|
+
end
|
125
|
+
|
126
|
+
def node_parsers
|
127
|
+
@node_parsers ||= Hash.new(:default_node_parser).merge!(
|
128
|
+
:class => :class_node_parser,
|
129
|
+
:module => :module_node_parser,
|
130
|
+
:def => :method_node_parser,
|
131
|
+
:send => :send_node_parser
|
132
|
+
# TODO: make a method parser, which pushes the the context_stack so that things inside method bodies
|
133
|
+
# are treated differently than those inside class or module bodies. same with Block (right?)
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
# search the whole tree for classes
|
138
|
+
def parse_ast(node=ast)
|
139
|
+
# files can start with (module), (class), or (begin)
|
140
|
+
# EDIT: or (send), or (def), or.... just about anything
|
141
|
+
return unless node.respond_to? :type
|
142
|
+
send(node_parsers[node.type], node)
|
143
|
+
end
|
144
|
+
|
145
|
+
def default_node_parser(node)
|
146
|
+
unhandled_node_types.add(node.type)
|
147
|
+
return unless node.respond_to? :children
|
148
|
+
node.children.each { |cnode| parse_ast(cnode) }
|
149
|
+
end
|
150
|
+
|
151
|
+
def class_node_parser(node)
|
152
|
+
name_node, super_node, content_node = node.children
|
153
|
+
klass = Entities::Class.new(context_stack.last, node)
|
154
|
+
@classes << klass
|
155
|
+
context_stack.push klass
|
156
|
+
parse_ast(content_node)
|
157
|
+
context_stack.pop
|
158
|
+
end
|
159
|
+
|
160
|
+
def module_node_parser(node)
|
161
|
+
name_node, content_node = node.children
|
162
|
+
mod = Entities::Module.new(context_stack.last, node)
|
163
|
+
context_stack.push mod
|
164
|
+
parse_ast(content_node)
|
165
|
+
context_stack.pop
|
166
|
+
end
|
167
|
+
|
168
|
+
def method_node_parser(node)
|
169
|
+
name, args_node, content_node = node.children
|
170
|
+
method = Entities::Method.new(context_stack.last, node)
|
171
|
+
context_stack.push method
|
172
|
+
parse_ast(content_node)
|
173
|
+
context_stack.pop
|
174
|
+
end
|
175
|
+
|
176
|
+
def send_node_parser(node)
|
177
|
+
target_node, method_name, *args = node.children
|
178
|
+
# if method_name is an association, then analyze the args to see what class it's associated with!
|
179
|
+
# and "it", btw, is the context_stack.last... so ya, also make sure this analysis is only done when we're in a
|
180
|
+
# class, NOT when we're in a method, NOT when we're in a defs, NOT when we're in an (sclass) (comes from
|
181
|
+
# class << self) -- got that?? those nodes should push something like 'Unsupported' onto the stack, or something
|
182
|
+
# just to keep track of where we are at any given point, and make sure we only care about sends that
|
183
|
+
# are truly at the right scope.
|
184
|
+
context_stack.last.handle_send_node(node)
|
185
|
+
# basically, if it's a class, it'll see if this is an association. if so, it'll store it by name. later on, we go
|
186
|
+
# thru and connect the pointers.
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
module Entities
|
192
|
+
|
193
|
+
class Entity
|
194
|
+
attr_reader :parent
|
195
|
+
|
196
|
+
def handle_send_node(node)
|
197
|
+
# abstract method. btw, this feels wrong -- send should be an entity too. but for now, whatevs.
|
198
|
+
end
|
199
|
+
|
200
|
+
def full_name
|
201
|
+
throw "this is abstract method, fool"
|
202
|
+
end
|
203
|
+
|
204
|
+
def inspect
|
205
|
+
"\#<#{self.class}:#{object_id} full_name=#{full_name}>"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
class Empty < Entity
|
211
|
+
def full_name
|
212
|
+
''
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
class Method < Entity
|
218
|
+
attr_reader :ast
|
219
|
+
|
220
|
+
def initialize(parent, ast)
|
221
|
+
@parent = parent
|
222
|
+
@ast = ast
|
223
|
+
end
|
224
|
+
|
225
|
+
def name
|
226
|
+
ast.children.first.to_s
|
227
|
+
end
|
228
|
+
|
229
|
+
def full_name
|
230
|
+
parent.full_name + '#' + name
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
class Module < Entity
|
236
|
+
attr_reader :ast
|
237
|
+
|
238
|
+
def initialize(parent, ast)
|
239
|
+
@parent = parent
|
240
|
+
@ast = ast
|
241
|
+
end
|
242
|
+
|
243
|
+
def name
|
244
|
+
const_node_array(ast.children.first).join('::')
|
245
|
+
end
|
246
|
+
|
247
|
+
def full_name
|
248
|
+
parent.full_name.empty? ? name : parent.full_name + '::' + name
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
# takes a (const) node and returns an array specifying the fully-qualified
|
254
|
+
# constant name that it represents. ya know, so CoolModule::SubMod::SweetClass
|
255
|
+
# would be parsed to:
|
256
|
+
# (const
|
257
|
+
# (const
|
258
|
+
# (const nil :CoolModule) :SubMod) :SweetClass)
|
259
|
+
# and passing that node here would return [:CoolModule, :SubMod, :SweetClass]
|
260
|
+
def const_node_array(node)
|
261
|
+
return [] if node.nil?
|
262
|
+
raise "expected (const) node or nil, got (#{node.type})" unless node.type == :const
|
263
|
+
const_node_array(node.children.first) << node.children[1]
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
class Class < Module
|
269
|
+
|
270
|
+
# pretend, for now, that we always have an AR. i.e., always look for associations.
|
271
|
+
ASSOCIATIONS = [:belongs_to, :has_one, :has_many, :has_and_belongs_to_many]
|
272
|
+
|
273
|
+
def handle_send_node(node)
|
274
|
+
# FIXME: this doesn't feel right, cuz (send) should probably be an entity too, especially
|
275
|
+
# since you can have nested sends... but i'm doing it this way for now.
|
276
|
+
target, method_name, *args = node.children
|
277
|
+
if ASSOCIATIONS.include? method_name
|
278
|
+
add_association(method_name, args)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def associations
|
283
|
+
@associations ||= []
|
284
|
+
end
|
285
|
+
|
286
|
+
def merge(other_class)
|
287
|
+
other_class.associations.each do |other_assoc|
|
288
|
+
duplicate = associations.detect do |assoc|
|
289
|
+
assoc.type == other_assoc.type && assoc.target_class == other_assoc.target_class
|
290
|
+
end
|
291
|
+
unless duplicate
|
292
|
+
associations << Association.new(type: other_assoc.type, source: self, target_class: other_assoc.target_class)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
def add_association(method_name, args)
|
300
|
+
if args.size > 1
|
301
|
+
# args.last is a hash that might contain a class_name or a through
|
302
|
+
target_class = hash_val_str(args.last, :class_name)
|
303
|
+
end
|
304
|
+
target_class ||= begin
|
305
|
+
symbol_node = args.first
|
306
|
+
symbol_name = symbol_node.children.first
|
307
|
+
table_name = ::ActiveSupport::Inflector.pluralize(symbol_name)
|
308
|
+
::ActiveSupport::Inflector.classify(table_name)
|
309
|
+
end
|
310
|
+
assoc = Association.new(type: method_name, source: self, target_class: target_class)
|
311
|
+
associations << assoc
|
312
|
+
end
|
313
|
+
|
314
|
+
# give it a (hash) node and a key (a symbol) and it'll look for that key in the hash and
|
315
|
+
# return the associated value as long as it's a string. if key isn't found, returns nil.
|
316
|
+
# if key is found but val isn't (str), throw exception. yep, this is pretty bespoke.
|
317
|
+
def hash_val_str(node, key)
|
318
|
+
return unless node.type == :hash
|
319
|
+
pair = node.children.detect do |pair_node|
|
320
|
+
key_sym_node = pair_node.children.first
|
321
|
+
key == key_sym_node.children.first
|
322
|
+
end
|
323
|
+
if pair
|
324
|
+
val_node = pair.children.last
|
325
|
+
throw "Bad type. Expected (str), got (#{val_node.type})" unless val_node.type == :str
|
326
|
+
val_node.children.first
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
class Association
|
335
|
+
attr_reader :type, :source, :target_class
|
336
|
+
attr_accessor :target
|
337
|
+
|
338
|
+
def initialize(type:, source:, target_class:)
|
339
|
+
@type = type
|
340
|
+
@source = source
|
341
|
+
@target_class = target_class
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class LineOfCode
|
2
|
+
|
3
|
+
include PoroPlus
|
4
|
+
include Ephemeral::Base
|
5
|
+
|
6
|
+
attr_accessor :line_number, :range, :content
|
7
|
+
|
8
|
+
def self.containing(locs, start_index, end_index)
|
9
|
+
locs.inject([]) do |a, loc|
|
10
|
+
a << loc if loc.in_range?(start_index) || loc.in_range?(end_index)
|
11
|
+
a
|
12
|
+
end.compact
|
13
|
+
end
|
14
|
+
|
15
|
+
def in_range?(index)
|
16
|
+
self.range.include?(index)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class ParsedFile
|
2
|
+
|
3
|
+
attr_accessor :lines_of_code, :source
|
4
|
+
attr_accessor :complexity, :path_to_file, :class_name, :path_to_results
|
5
|
+
|
6
|
+
def initialize(path_to_file: path_to_file, class_name: class_name=nil, complexity: complexity)
|
7
|
+
@path_to_file = path_to_file
|
8
|
+
@class_name = class_name
|
9
|
+
@lines_of_code = []
|
10
|
+
@complexity = complexity
|
11
|
+
@source = parse!
|
12
|
+
end
|
13
|
+
|
14
|
+
def class_name
|
15
|
+
@class_name ||= analyzer.class_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def class_references
|
19
|
+
@class_references ||= analyzer.constants
|
20
|
+
end
|
21
|
+
|
22
|
+
def average_complexity
|
23
|
+
methods.map(&:complexity).reduce(:+) / methods.count.to_f
|
24
|
+
end
|
25
|
+
|
26
|
+
def complexity
|
27
|
+
@complexity ||= analyzer.complexity
|
28
|
+
end
|
29
|
+
|
30
|
+
def methods
|
31
|
+
@methods ||= analyzer.methods
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_counts
|
35
|
+
referenced_methods = methods.map(&:references).flatten
|
36
|
+
referenced_methods.inject({}) do |hash, method|
|
37
|
+
hash[method] ||= 0
|
38
|
+
hash[method] += 1
|
39
|
+
hash
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def source
|
44
|
+
return @source if @source
|
45
|
+
end_pos = 0
|
46
|
+
self.lines_of_code = []
|
47
|
+
@source = File.readlines(self.path_to_file).each_with_index do |line, index|
|
48
|
+
start_pos = end_pos + 1
|
49
|
+
end_pos += line.size
|
50
|
+
self.lines_of_code << LineOfCode.new(line_number: index + 1, range: (start_pos..end_pos))
|
51
|
+
line
|
52
|
+
end.join
|
53
|
+
end
|
54
|
+
|
55
|
+
def summary
|
56
|
+
{
|
57
|
+
path_to_file: self.path_to_file,
|
58
|
+
results_file: self.path_to_results,
|
59
|
+
source: source,
|
60
|
+
class_name: self.class_name,
|
61
|
+
complexity: complexity
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def analyzer
|
68
|
+
@analyzer ||= Analyzer.new(content)
|
69
|
+
end
|
70
|
+
|
71
|
+
def content
|
72
|
+
@content ||= File.open(path_to_file, "r").read
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse!
|
76
|
+
end_pos = 0
|
77
|
+
File.readlines(self.path_to_file).each_with_index do |line, index|
|
78
|
+
start_pos = end_pos + 1
|
79
|
+
end_pos += line.size
|
80
|
+
self.lines_of_code << LineOfCode.new(line_number: index + 1, range: (start_pos..end_pos))
|
81
|
+
line
|
82
|
+
end.join
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class ParsedMethod
|
2
|
+
|
3
|
+
attr_reader :name, :content, :type, :complexity, :references
|
4
|
+
|
5
|
+
def initialize(name: name, content: content, type: type, refs: refs=[], complexity: complexity)
|
6
|
+
@name = name
|
7
|
+
@content = content
|
8
|
+
@type = type
|
9
|
+
@references = refs
|
10
|
+
@complexity = complexity
|
11
|
+
end
|
12
|
+
|
13
|
+
def complexity
|
14
|
+
@complexity ||= analyzer.complexity
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
return "" if self.type == :none
|
19
|
+
"#{prefix}#{@name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def prefix
|
23
|
+
return "." if self.type == :class
|
24
|
+
return "#" if self.type == :instance
|
25
|
+
return "*"
|
26
|
+
end
|
27
|
+
|
28
|
+
def analyzer
|
29
|
+
Analyzer.new(self.content)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Analyst
|
4
|
+
|
5
|
+
class Parser
|
6
|
+
|
7
|
+
attr_reader :start_path, :parsed_files
|
8
|
+
attr_reader :threshold, :formatter
|
9
|
+
attr_reader :start_time
|
10
|
+
|
11
|
+
OUTPUT_DIRECTORY = "doc/Analyst"
|
12
|
+
|
13
|
+
def initialize(path, formatter, threshold=0)
|
14
|
+
@start_path = path
|
15
|
+
@formatter = formatter
|
16
|
+
@threshold = threshold
|
17
|
+
@start_time = Time.now
|
18
|
+
reset_output_directory
|
19
|
+
end
|
20
|
+
|
21
|
+
def parsed_files
|
22
|
+
@parsed_files = source_files.map do |path_to_file|
|
23
|
+
parse_source_file(path_to_file)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def report
|
28
|
+
self.parsed_files.each do |file|
|
29
|
+
print "."
|
30
|
+
formatter.new(file, OUTPUT_DIRECTORY, file.source).export
|
31
|
+
end
|
32
|
+
puts
|
33
|
+
write_report_index
|
34
|
+
report_complexity
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def reset_output_directory
|
40
|
+
begin
|
41
|
+
FileUtils.remove_dir(OUTPUT_DIRECTORY)
|
42
|
+
rescue Errno::ENOENT
|
43
|
+
end
|
44
|
+
FileUtils.mkpath(OUTPUT_DIRECTORY)
|
45
|
+
end
|
46
|
+
|
47
|
+
def source_files
|
48
|
+
if File.directory?(start_path)
|
49
|
+
return Dir.glob(File.join(start_path, "**", "*.rb"))
|
50
|
+
else
|
51
|
+
return [start_path]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_source_file(path_to_file, options={})
|
56
|
+
ParsedFile.new(path_to_file: path_to_file)
|
57
|
+
end
|
58
|
+
|
59
|
+
def report_complexity
|
60
|
+
return if self.threshold == 0
|
61
|
+
complexities = self.parsed_files.map(&:complexity)
|
62
|
+
return if complexities.max.to_i <= self.threshold
|
63
|
+
puts "Maximum complexity of #{complexities.max} exceeds #{options['threshold']} threshold!"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
def write_report_index
|
68
|
+
return unless self.formatter.writes_to_file_system?
|
69
|
+
puts "Results written to #{OUTPUT_DIRECTORY} "
|
70
|
+
return unless self.formatter.has_index?
|
71
|
+
formatter.index_class.new(parsed_files.map(&:summary), OUTPUT_DIRECTORY).export
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|