analyst 0.0.1 → 0.13.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|