reconn 0.1.0
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 +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +4 -0
- data/bin/reconn +13 -0
- data/lib/reconn.rb +5 -0
- data/lib/reconn/analyzer.rb +201 -0
- data/lib/reconn/analyzer/code_smell.rb +16 -0
- data/lib/reconn/analyzer/project_elements/class.rb +48 -0
- data/lib/reconn/analyzer/project_elements/method.rb +28 -0
- data/lib/reconn/util/project_scanner.rb +28 -0
- data/lib/reconn/version.rb +3 -0
- data/lib/reconn/view/glade/View.glade +452 -0
- data/lib/reconn/view/view.rb +277 -0
- data/lib/reconn/visualizer.rb +44 -0
- data/reconn.gemspec +31 -0
- data/spec/reconn/analyzer/analyzer_spec.rb +33 -0
- data/spec/reconn/analyzer/code_smells/code_smell_spec.rb +0 -0
- data/spec/reconn/analyzer/code_smells/if_smell_spec.rb +0 -0
- data/spec/reconn/analyzer/code_smells/too_big_method_smell_spec.rb +0 -0
- data/spec/reconn/analyzer/project_elements/class_spec.rb +9 -0
- data/spec/reconn/analyzer/project_elements/method_spec.rb +9 -0
- data/spec/reconn/util/project_scanner_spec.rb +20 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/test_projects/empty_project/not_rb_file +0 -0
- data/spec/test_projects/multiple_files_project/analyzer.rb +111 -0
- data/spec/test_projects/multiple_files_project/view.rb +189 -0
- data/spec/test_projects/one_file_project/analyzer.rb +111 -0
- metadata +226 -0
@@ -0,0 +1,277 @@
|
|
1
|
+
require_relative '../analyzer.rb'
|
2
|
+
require_relative '../visualizer.rb'
|
3
|
+
|
4
|
+
class View
|
5
|
+
|
6
|
+
include GladeGUI
|
7
|
+
|
8
|
+
def before_show()
|
9
|
+
dialog_response, dialog = show_file_chooser_dialog
|
10
|
+
if dialog_response == Gtk::Dialog::RESPONSE_ACCEPT
|
11
|
+
analyzer = Analyzer::Analyzer.new
|
12
|
+
@classes, @methods, @smells = analyzer.analyze(dialog.filename)
|
13
|
+
|
14
|
+
@general_stats_view = build_general_stats_view
|
15
|
+
@builder['dataviewport'].add(@general_stats_view)
|
16
|
+
end
|
17
|
+
dialog.destroy
|
18
|
+
end
|
19
|
+
|
20
|
+
#########################################
|
21
|
+
# On button clicked methods
|
22
|
+
|
23
|
+
def on_general_stats_button_clicked
|
24
|
+
clean_data_view
|
25
|
+
|
26
|
+
unless @general_stats_view
|
27
|
+
@general_stats_view = build_general_stats_view
|
28
|
+
end
|
29
|
+
|
30
|
+
@builder['dataviewport'].add(@general_stats_view)
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_class_stats_button_clicked
|
34
|
+
clean_data_view
|
35
|
+
|
36
|
+
unless @class_stats_view
|
37
|
+
@class_stats_view = build_class_stats_view
|
38
|
+
end
|
39
|
+
|
40
|
+
@builder['dataviewport'].add(@class_stats_view)
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_class_diag_button_clicked
|
44
|
+
clean_data_view
|
45
|
+
|
46
|
+
unless @class_diag_view
|
47
|
+
@class_diag_view = build_class_diag_view
|
48
|
+
end
|
49
|
+
|
50
|
+
@builder['dataviewport'].add(@class_diag_view)
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_class_dep_button_clicked
|
54
|
+
clean_data_view
|
55
|
+
|
56
|
+
unless @class_dep_view
|
57
|
+
@class_dep_view = build_class_dep_view
|
58
|
+
end
|
59
|
+
|
60
|
+
@builder['dataviewport'].add(@class_dep_view)
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_method_stats_button_clicked
|
64
|
+
clean_data_view
|
65
|
+
|
66
|
+
unless @method_stats_view
|
67
|
+
@method_stats_view = build_method_stats_view
|
68
|
+
end
|
69
|
+
|
70
|
+
@builder['dataviewport'].add(@method_stats_view)
|
71
|
+
end
|
72
|
+
|
73
|
+
def on_method_diag_button_clicked
|
74
|
+
clean_data_view
|
75
|
+
|
76
|
+
unless @method_diag_view
|
77
|
+
@method_diag_view = build_method_diag_view
|
78
|
+
end
|
79
|
+
|
80
|
+
@builder['dataviewport'].add(@method_diag_view)
|
81
|
+
end
|
82
|
+
|
83
|
+
def on_if_smell_button_clicked
|
84
|
+
clean_data_view
|
85
|
+
|
86
|
+
unless @if_smell_view
|
87
|
+
@if_smell_view = build_if_smell_view
|
88
|
+
end
|
89
|
+
|
90
|
+
@builder['dataviewport'].add(@if_smell_view)
|
91
|
+
end
|
92
|
+
|
93
|
+
def on_method_smell_button_clicked
|
94
|
+
clean_data_view
|
95
|
+
|
96
|
+
unless @method_smell_view
|
97
|
+
@method_smell_view = build_method_smell_view
|
98
|
+
end
|
99
|
+
|
100
|
+
@builder['dataviewport'].add(@method_smell_view)
|
101
|
+
end
|
102
|
+
|
103
|
+
###########################################
|
104
|
+
# View builders
|
105
|
+
|
106
|
+
def build_general_stats_view
|
107
|
+
lines = 0
|
108
|
+
@methods.each do |method|
|
109
|
+
lines += method.lines
|
110
|
+
end
|
111
|
+
|
112
|
+
text_arr = ["Total number of classes: #{@classes.length} \n"]
|
113
|
+
text_arr << "Total number of methods: #{@methods.length} \n"
|
114
|
+
text_arr << "Total number of lines of code: #{lines} \n"
|
115
|
+
|
116
|
+
largest_class = @classes.sort_by {|c| c.lines}.pop
|
117
|
+
|
118
|
+
class_methodnum = Hash.new(0)
|
119
|
+
@classes.each do |klass|
|
120
|
+
class_methodnum.store(klass.name, klass.methods.size)
|
121
|
+
end
|
122
|
+
class_methodnum = class_methodnum.sort_by {|name, methods| methods}
|
123
|
+
|
124
|
+
text_arr << "\n"
|
125
|
+
text_arr << "Largest class: #{largest_class} \n"
|
126
|
+
text_arr << "Number of lines: #{largest_class.lines} \n"
|
127
|
+
text_arr << "\n"
|
128
|
+
text_arr << "Class with largest number of methods: #{class_methodnum.last[0]} \n"
|
129
|
+
text_arr << "Number of methods: #{class_methodnum.last[1]} \n"
|
130
|
+
|
131
|
+
largest_method = @methods.sort_by {|m| m.lines}.pop
|
132
|
+
text_arr << "\n"
|
133
|
+
text_arr << "Largest method: #{largest_method} \n"
|
134
|
+
text_arr << "Number of lines: #{largest_method.lines} \n"
|
135
|
+
|
136
|
+
build_text_view(text_arr.join)
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_class_stats_view
|
140
|
+
features = [ {name: "Number of lines", data: prepare_data(@classes, :lines)} ]
|
141
|
+
features << {name: "Cyclomatic complexity", data: prepare_data(@classes, :complexity)}
|
142
|
+
features << {name: "Number of methods", data: prepare_data(@classes, :methods_number)}
|
143
|
+
text = prepare_text(features)
|
144
|
+
build_text_view(text)
|
145
|
+
end
|
146
|
+
|
147
|
+
def build_method_stats_view
|
148
|
+
features = [ {name: "Number of lines", data: prepare_data(@methods, :lines)} ]
|
149
|
+
features << {name: "Cyclomatic complexity", data: prepare_data(@methods, :complexity)}
|
150
|
+
text = prepare_text(features)
|
151
|
+
build_text_view(text)
|
152
|
+
end
|
153
|
+
|
154
|
+
def build_if_smell_view
|
155
|
+
text_arr = []
|
156
|
+
@smells.each {|s| text_arr << s.to_s + "\n" if s.type == :too_complex_method}
|
157
|
+
build_text_view(text_arr.join)
|
158
|
+
end
|
159
|
+
|
160
|
+
def build_method_smell_view
|
161
|
+
text_arr = []
|
162
|
+
@smells.each {|s| text_arr << s.to_s + "\n" if s.type == :too_big_method}
|
163
|
+
build_text_view(text_arr.join)
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_text_view(text)
|
167
|
+
stats_view = Gtk::TextView.new
|
168
|
+
stats_view.editable = false
|
169
|
+
stats_view.cursor_visible = false
|
170
|
+
stats_view.buffer.text = text
|
171
|
+
stats_view.show
|
172
|
+
stats_view
|
173
|
+
end
|
174
|
+
|
175
|
+
def prepare_text(features)
|
176
|
+
text_arr = []
|
177
|
+
features.each do |feature|
|
178
|
+
text_arr << "#{feature[:name]}: \n"
|
179
|
+
data = feature[:data]
|
180
|
+
data.each_index do |i|
|
181
|
+
break if i == 30
|
182
|
+
item = data[i]
|
183
|
+
text_arr << "#{i+1}. #{item[:label]} => #{item[:value]} \n"
|
184
|
+
end
|
185
|
+
text_arr << "\n"
|
186
|
+
end
|
187
|
+
text_arr.join
|
188
|
+
end
|
189
|
+
|
190
|
+
def build_class_diag_view
|
191
|
+
diagrams = [ {title: "Lines of code", parameter: :lines} ]
|
192
|
+
diagrams << {title: "Cyclomatic complexity", parameter: :complexity}
|
193
|
+
diagrams << {title: "Number of methods", parameter: "methods_number"}
|
194
|
+
build_diag_view(@classes, diagrams)
|
195
|
+
end
|
196
|
+
|
197
|
+
def build_method_diag_view
|
198
|
+
diagrams = [ {title: "Lines of code", parameter: :lines} ]
|
199
|
+
diagrams << {title: "Cyclomatic complexity", parameter: :complexity}
|
200
|
+
build_diag_view(@methods, diagrams)
|
201
|
+
end
|
202
|
+
|
203
|
+
def build_diag_view(raw_data, diagrams)
|
204
|
+
tabbed_panel = Gtk::Notebook.new
|
205
|
+
|
206
|
+
diagrams.each do |diag|
|
207
|
+
data = prepare_data(raw_data, diag[:parameter])
|
208
|
+
|
209
|
+
title = diag[:title]
|
210
|
+
|
211
|
+
pie_chart = build_pie_chart(title, data)
|
212
|
+
bar_chart = build_bar_chart(title, data.first(10))
|
213
|
+
|
214
|
+
container = Gtk::VBox.new(false, 4)
|
215
|
+
container = container.pack_end(pie_chart)
|
216
|
+
container = container.pack_end(bar_chart)
|
217
|
+
container.show
|
218
|
+
|
219
|
+
tabbed_panel.append_page(container, Gtk::Label.new(title))
|
220
|
+
end
|
221
|
+
|
222
|
+
tabbed_panel.show
|
223
|
+
end
|
224
|
+
|
225
|
+
def build_pie_chart(title, data)
|
226
|
+
binary_chart = Visualizer.make_pie_chart(title, data, 4)
|
227
|
+
chart_to_image(binary_chart)
|
228
|
+
end
|
229
|
+
|
230
|
+
def build_bar_chart(title, data)
|
231
|
+
binary_chart = Visualizer.make_bar_chart(title, data)
|
232
|
+
chart_to_image(binary_chart)
|
233
|
+
end
|
234
|
+
|
235
|
+
def chart_to_image(binary_chart)
|
236
|
+
loader = Gdk::PixbufLoader.new("png")
|
237
|
+
loader.last_write(binary_chart)
|
238
|
+
chart = loader.pixbuf
|
239
|
+
Gtk::Image.new(chart).show
|
240
|
+
end
|
241
|
+
#######
|
242
|
+
def build_class_dep_view
|
243
|
+
binary_chart = Visualizer.make_dependency_diagram(@classes)
|
244
|
+
loader = Gdk::PixbufLoader.new("png")
|
245
|
+
loader.last_write(binary_chart)
|
246
|
+
chart = loader.pixbuf
|
247
|
+
Gtk::Image.new(chart).show
|
248
|
+
end
|
249
|
+
|
250
|
+
##########################################
|
251
|
+
|
252
|
+
def show_file_chooser_dialog
|
253
|
+
dialog = Gtk::FileChooserDialog.new("Open File",
|
254
|
+
nil,
|
255
|
+
Gtk::FileChooser::ACTION_SELECT_FOLDER,
|
256
|
+
nil,
|
257
|
+
[Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
|
258
|
+
[Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT])
|
259
|
+
dialog_response = dialog.run
|
260
|
+
return dialog_response, dialog
|
261
|
+
end
|
262
|
+
|
263
|
+
def clean_data_view
|
264
|
+
@builder['dataviewport'].each do |child|
|
265
|
+
@builder['dataviewport'].remove(child)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def prepare_data(raw_data, parameter)
|
270
|
+
data = raw_data.sort_by {|d| d.send(parameter.to_s)}.reverse
|
271
|
+
data.map! {|d| {label: d.to_s, value: d.send(parameter.to_s)}}
|
272
|
+
end
|
273
|
+
|
274
|
+
private :build_general_stats_view, :build_class_stats_view,
|
275
|
+
:build_method_stats_view, :build_if_smell_view,
|
276
|
+
:build_method_smell_view, :build_text_view
|
277
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'gruff'
|
2
|
+
require 'ruby-graphviz'
|
3
|
+
|
4
|
+
class Visualizer
|
5
|
+
def self.make_pie_chart(title, data, items_number)
|
6
|
+
chart = Gruff::Pie.new
|
7
|
+
chart.title = title.to_s
|
8
|
+
data = Array.new(data)
|
9
|
+
data.first(items_number).each do |item|
|
10
|
+
chart.data(item[:label], item[:value])
|
11
|
+
data.delete(item)
|
12
|
+
end
|
13
|
+
|
14
|
+
chart.data("other", data.map {|itm| itm[:value]}.inject(:+))
|
15
|
+
|
16
|
+
chart.to_blob
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.make_bar_chart(title, data)
|
20
|
+
chart = Gruff::Bar.new
|
21
|
+
chart.minimum_value = 0
|
22
|
+
chart.maximum_value = data.first[:value]
|
23
|
+
chart.title = title.to_s
|
24
|
+
data.each do |item|
|
25
|
+
chart.data(item[:label], item[:value])
|
26
|
+
end
|
27
|
+
|
28
|
+
chart.to_blob
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.make_dependency_diagram(classes)
|
32
|
+
diagram = GraphViz.new(:G, :type => :digraph)
|
33
|
+
nodes = classes.map { |c| diagram.add_nodes(c.name) }
|
34
|
+
classes.each do |klass|
|
35
|
+
classes.each do |other_klass|
|
36
|
+
if !klass.dependencies.index {|d| d == other_klass.name }.nil?
|
37
|
+
node, other_node = [klass, other_klass].map {|k| nodes.find {|n| n[:label].to_s.gsub('"', '') == k.name}}
|
38
|
+
diagram.add_edges(node, other_node)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
diagram.output(:png => String)
|
43
|
+
end
|
44
|
+
end
|
data/reconn.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'reconn/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "reconn"
|
8
|
+
spec.version = Reconn::VERSION
|
9
|
+
spec.authors = ["Mateusz Czarnecki"]
|
10
|
+
spec.email = ["mateusz.czarnecki92@gmail.com"]
|
11
|
+
spec.summary = %q{A tool for analysis and visualization of projects written in Ruby}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "vrlib", ">= 1.0.16"
|
21
|
+
spec.add_dependency "gtk2", ">= 2.2.0"
|
22
|
+
spec.add_dependency "require_all", ">= 1.3.2"
|
23
|
+
spec.add_dependency "ruby_parser", ">= 3.6.3"
|
24
|
+
spec.add_dependency "sexp_processor", ">= 4.4.4"
|
25
|
+
spec.add_dependency "gruff", ">= 0.5.1"
|
26
|
+
spec.add_dependency "ruby-graphviz", ">= 1.2.1"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
29
|
+
spec.add_development_dependency "rake"
|
30
|
+
spec.add_development_dependency "rspec"
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Analyzer do
|
4
|
+
describe '#analyze' do
|
5
|
+
before :all do
|
6
|
+
@result = Analyzer::Analyzer.new.analyze(File.dirname(__FILE__) + '/../../../lib')
|
7
|
+
@classes, @methods, @smells = @result
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'returns an Array' do
|
11
|
+
expect( @result.respond_to?(:to_ary) ).to be true
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'returns classes data' do
|
15
|
+
expect( @classes ).not_to be_empty
|
16
|
+
expect( @classes.map {|x| x.name } ).to include('Analyzer')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns methods data' do
|
20
|
+
expect( @methods ).not_to be_empty
|
21
|
+
expect( @methods.map {|x| x.name } ).to include('analyze')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'calculates lines of code in classes' do
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'calculates lines of code in methods' do
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'finds dependencies between classes' do
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ProjectScanner do
|
4
|
+
describe '::scan' do
|
5
|
+
it 'should scan the project directory' +
|
6
|
+
' and return paths to ruby source files when the path is correct and the directory has .rb files' do
|
7
|
+
paths = ProjectScanner.scan(File.dirname(__FILE__) + '/../../test_projects')
|
8
|
+
expect(paths).not_to be_empty
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should raise an exception when the project directory path is invalid' do
|
12
|
+
expect { ProjectScanner.scan('a_directory')}.to raise_error
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should return empty array when no ruby files were found' do
|
16
|
+
paths = ProjectScanner.scan(File.dirname(__FILE__) + '/../../test_projects/empty_project')
|
17
|
+
expect(paths).to be_empty
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/spec/spec_helper.rb
ADDED
File without changes
|