reconn 0.1.0 → 1.0.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 +4 -4
- data/bin/reconn +1 -1
- data/lib/reconn/analyzer.rb +35 -10
- data/lib/reconn/analyzer/code_smell.rb +11 -11
- data/lib/reconn/analyzer/project_elements/class.rb +7 -3
- data/lib/reconn/analyzer/project_elements/method.rb +11 -3
- data/lib/reconn/util/project_scanner.rb +22 -20
- data/lib/reconn/version.rb +1 -1
- data/lib/reconn/view/view.rb +7 -5
- data/lib/reconn/visualizer.rb +149 -28
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98a521e90dd569d2e385655e695c2b20b573c72a
|
4
|
+
data.tar.gz: 68dea3d08b90bca2e6790d675970783886bab1d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 50359a392132d7e1bfff84fef8afc3fa4bd0dd8c8504a54dd4d7263630c6e38c0b90f0560142325417e18b4eb43511b2e300a4a6bddef5fc3788e085d5c20bc1
|
7
|
+
data.tar.gz: b2de99d4e108c48ecf5f0acc30f1080fd1639dbcf5cd74eb2a49f36ab70346436688c7d57a4c0d06d335bbed63b467b341ad82d86ae4a6cba3c38c3591690b99
|
data/bin/reconn
CHANGED
data/lib/reconn/analyzer.rb
CHANGED
@@ -2,10 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'ruby_parser'
|
4
4
|
require 'sexp_processor'
|
5
|
-
require_relative 'util/project_scanner'
|
6
|
-
require_relative 'analyzer/project_elements/class.rb'
|
7
|
-
require_relative 'analyzer/project_elements/method.rb'
|
8
5
|
|
6
|
+
module Reconn
|
9
7
|
module Analyzer
|
10
8
|
class Analyzer < MethodBasedSexpProcessor
|
11
9
|
|
@@ -43,9 +41,10 @@ module Analyzer
|
|
43
41
|
klass.complexity = count_complexity_in_class(klass)
|
44
42
|
end
|
45
43
|
prune_dependencies
|
44
|
+
find_external_dependencies(paths)
|
46
45
|
|
47
46
|
# Deletes empty classes
|
48
|
-
|
47
|
+
#@classes.delete(Class.new(:none))
|
49
48
|
|
50
49
|
@smells = find_code_smells
|
51
50
|
|
@@ -58,7 +57,7 @@ module Analyzer
|
|
58
57
|
def process_class(exp)
|
59
58
|
exp.shift
|
60
59
|
in_klass(exp.shift) do
|
61
|
-
@current_class = Class.new(klass_name)
|
60
|
+
@current_class = Class.new(klass_name, [@current_path.to_s])
|
62
61
|
@classes << @current_class
|
63
62
|
process_until_empty exp
|
64
63
|
end
|
@@ -68,10 +67,10 @@ module Analyzer
|
|
68
67
|
end
|
69
68
|
|
70
69
|
def process_defn(exp)
|
71
|
-
exp.shift
|
70
|
+
is_singleton = exp.shift.to_s == "defn" ? false : true
|
72
71
|
method_name = exp.shift.to_s
|
73
72
|
lines = count_lines_in_method(method_name)
|
74
|
-
@current_method = Method.new(method_name, @current_class.name, lines)
|
73
|
+
@current_method = Method.new(method_name, @current_path.to_s, @current_class.name, lines, is_singleton)
|
75
74
|
exp.shift
|
76
75
|
process_until_empty exp
|
77
76
|
|
@@ -163,8 +162,15 @@ module Analyzer
|
|
163
162
|
@classes.each do |klass|
|
164
163
|
klass.dependencies = klass.dependencies.map do |dep|
|
165
164
|
dep_split = dep.split('::')
|
165
|
+
if class_names.include?(dep)
|
166
|
+
next dep
|
167
|
+
end
|
166
168
|
klass_split = klass.name.split('::')
|
167
|
-
klass_split.
|
169
|
+
if klass_split.size != dep_split.size
|
170
|
+
klass_split.pop(dep_split.size)
|
171
|
+
else
|
172
|
+
klass_split.pop(dep_split.size - 1)
|
173
|
+
end
|
168
174
|
(klass_split + dep_split).join('::')
|
169
175
|
end
|
170
176
|
klass.dependencies = klass.dependencies.uniq.keep_if {|dep| dep != klass.name && class_names.include?(dep)}
|
@@ -184,14 +190,32 @@ module Analyzer
|
|
184
190
|
end
|
185
191
|
end
|
186
192
|
|
193
|
+
def find_external_dependencies(paths)
|
194
|
+
@classes.each do |klass|
|
195
|
+
external_deps = []
|
196
|
+
klass.filepaths.each do |path|
|
197
|
+
File.foreach(path) do |line|
|
198
|
+
line.strip!
|
199
|
+
if line =~ /^require .*$/
|
200
|
+
dep = line.split(" ")[1].gsub(/([\"\'])/, "")
|
201
|
+
external_deps << dep if !paths.find {|p| p.to_s =~ /.*#{dep}.*/}
|
202
|
+
else
|
203
|
+
next
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
klass.external_deps = external_deps
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
187
211
|
def find_code_smells
|
188
212
|
code_smells = []
|
189
213
|
@methods.each do |method|
|
190
214
|
if method.lines > MAX_METHOD_LENGTH
|
191
|
-
code_smells << CodeSmell.new(:too_big_method, method
|
215
|
+
code_smells << CodeSmell.new(:too_big_method, method)
|
192
216
|
end
|
193
217
|
if method.complexity > MAX_COMPLEXITY
|
194
|
-
code_smells << CodeSmell.new(:too_complex_method, method
|
218
|
+
code_smells << CodeSmell.new(:too_complex_method, method)
|
195
219
|
end
|
196
220
|
end
|
197
221
|
code_smells
|
@@ -199,3 +223,4 @@ module Analyzer
|
|
199
223
|
|
200
224
|
end
|
201
225
|
end
|
226
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
1
|
+
module Reconn
|
2
|
+
module Analyzer
|
3
|
+
class CodeSmell
|
4
|
+
attr_reader :type, :method
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
def initialize(type, method)
|
7
|
+
@type = type
|
8
|
+
@method = method
|
9
|
+
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def to_s
|
12
|
+
"Smell: #{@type.to_s} in #{@method.to_s} in file: #{method.filepath}"
|
13
|
+
end
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -1,8 +1,9 @@
|
|
1
|
+
module Reconn
|
1
2
|
module Analyzer
|
2
3
|
# Represents a class in the project
|
3
4
|
class Class
|
4
|
-
attr_reader :name, :methods
|
5
|
-
attr_accessor :lines, :complexity, :dependencies
|
5
|
+
attr_reader :name, :methods, :filepaths
|
6
|
+
attr_accessor :lines, :complexity, :dependencies, :external_deps
|
6
7
|
|
7
8
|
include Comparable
|
8
9
|
|
@@ -10,8 +11,9 @@ module Analyzer
|
|
10
11
|
name == other.name
|
11
12
|
end
|
12
13
|
|
13
|
-
def initialize(name)
|
14
|
+
def initialize(name, filepaths = [])
|
14
15
|
@name = name
|
16
|
+
@filepaths = filepaths
|
15
17
|
@dependencies = []
|
16
18
|
@methods = []
|
17
19
|
@lines = 0
|
@@ -37,6 +39,7 @@ module Analyzer
|
|
37
39
|
end
|
38
40
|
end
|
39
41
|
@dependencies += other.dependencies
|
42
|
+
@filepaths += other.filepaths
|
40
43
|
self
|
41
44
|
end
|
42
45
|
|
@@ -46,3 +49,4 @@ module Analyzer
|
|
46
49
|
|
47
50
|
end
|
48
51
|
end
|
52
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
|
+
module Reconn
|
1
2
|
module Analyzer
|
2
3
|
# Represents a method in the project
|
3
4
|
class Method
|
4
|
-
attr_reader :name, :class_name, :lines, :complexity
|
5
|
+
attr_reader :name, :class_name, :lines, :complexity, :filepath
|
5
6
|
|
6
7
|
include Comparable
|
7
8
|
|
@@ -9,20 +10,27 @@ module Analyzer
|
|
9
10
|
name == other.name && class_name == other.class_name
|
10
11
|
end
|
11
12
|
|
12
|
-
def initialize(name, class_name = :none, lines = 0)
|
13
|
+
def initialize(name, filepath = "", class_name = :none, lines = 0, is_singleton = false)
|
13
14
|
@name = name
|
15
|
+
@filepath = filepath
|
14
16
|
@class_name = class_name
|
15
17
|
@lines = lines
|
16
18
|
@complexity = 1
|
19
|
+
@is_singleton = is_singleton
|
17
20
|
end
|
18
21
|
|
19
22
|
def to_s
|
20
|
-
name.to_s
|
23
|
+
class_name.to_s + (is_singleton? ? "::" : "#") + name.to_s
|
21
24
|
end
|
22
25
|
|
23
26
|
def incr_complexity
|
24
27
|
@complexity += 1
|
25
28
|
end
|
26
29
|
|
30
|
+
def is_singleton?
|
31
|
+
@is_singleton
|
32
|
+
end
|
33
|
+
|
27
34
|
end
|
28
35
|
end
|
36
|
+
end
|
@@ -1,28 +1,30 @@
|
|
1
1
|
require 'find'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
if
|
15
|
-
|
3
|
+
module Reconn
|
4
|
+
class ProjectScanner
|
5
|
+
# Scans the given directory and all its subdirectories for ruby files
|
6
|
+
#
|
7
|
+
# @param proj_path [String] path to the project directory
|
8
|
+
# @return [Array<String>] paths to the ruby files
|
9
|
+
# @raise [InvalidPathException] if it can't open the directory
|
10
|
+
def self.scan(proj_path)
|
11
|
+
paths = []
|
12
|
+
begin
|
13
|
+
Find.find(proj_path) do |path|
|
14
|
+
if FileTest.directory?(path)
|
15
|
+
if File.basename(path)[0] == '.'
|
16
|
+
Find.prune
|
17
|
+
end
|
18
|
+
end
|
19
|
+
if File.extname(path) == '.rb'
|
20
|
+
paths << path
|
16
21
|
end
|
17
22
|
end
|
18
|
-
|
19
|
-
|
20
|
-
end
|
23
|
+
rescue
|
24
|
+
raise InvalidPathException, "Can't open the directory"
|
21
25
|
end
|
22
|
-
rescue
|
23
|
-
raise InvalidPathException, "Can't open the directory"
|
24
|
-
end
|
25
26
|
|
26
|
-
|
27
|
+
paths
|
28
|
+
end
|
27
29
|
end
|
28
30
|
end
|
data/lib/reconn/version.rb
CHANGED
data/lib/reconn/view/view.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative '../analyzer.rb'
|
2
2
|
require_relative '../visualizer.rb'
|
3
3
|
|
4
|
+
module Reconn
|
4
5
|
class View
|
5
6
|
|
6
7
|
include GladeGUI
|
@@ -195,8 +196,8 @@ class View
|
|
195
196
|
end
|
196
197
|
|
197
198
|
def build_method_diag_view
|
198
|
-
diagrams = [ {title: "Lines of code", parameter: :lines} ]
|
199
|
-
diagrams << {title: "Cyclomatic complexity", parameter: :complexity}
|
199
|
+
diagrams = [ {title: "Lines of code", parameter: :lines, treshold: Analyzer::Analyzer::MAX_METHOD_LENGTH} ]
|
200
|
+
diagrams << {title: "Cyclomatic complexity", parameter: :complexity, treshold: Analyzer::Analyzer::MAX_COMPLEXITY}
|
200
201
|
build_diag_view(@methods, diagrams)
|
201
202
|
end
|
202
203
|
|
@@ -209,7 +210,7 @@ class View
|
|
209
210
|
title = diag[:title]
|
210
211
|
|
211
212
|
pie_chart = build_pie_chart(title, data)
|
212
|
-
bar_chart = build_bar_chart(title, data.first(10))
|
213
|
+
bar_chart = build_bar_chart(title, data.first(10), diag[:treshold])
|
213
214
|
|
214
215
|
container = Gtk::VBox.new(false, 4)
|
215
216
|
container = container.pack_end(pie_chart)
|
@@ -227,8 +228,8 @@ class View
|
|
227
228
|
chart_to_image(binary_chart)
|
228
229
|
end
|
229
230
|
|
230
|
-
def build_bar_chart(title, data)
|
231
|
-
binary_chart = Visualizer.make_bar_chart(title, data)
|
231
|
+
def build_bar_chart(title, data, treshold)
|
232
|
+
binary_chart = Visualizer.make_bar_chart(title, data, treshold)
|
232
233
|
chart_to_image(binary_chart)
|
233
234
|
end
|
234
235
|
|
@@ -275,3 +276,4 @@ class View
|
|
275
276
|
:build_method_stats_view, :build_if_smell_view,
|
276
277
|
:build_method_smell_view, :build_text_view
|
277
278
|
end
|
279
|
+
end
|
data/lib/reconn/visualizer.rb
CHANGED
@@ -1,44 +1,165 @@
|
|
1
1
|
require 'gruff'
|
2
2
|
require 'ruby-graphviz'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
module Reconn
|
5
|
+
class Visualizer
|
6
|
+
def self.make_pie_chart(title, data, items_number)
|
7
|
+
chart = Gruff::Pie.new
|
8
|
+
chart.title = title.to_s
|
9
|
+
data = Array.new(data)
|
10
|
+
data.first(items_number).each do |item|
|
11
|
+
chart.data(item[:label], item[:value])
|
12
|
+
data.delete(item)
|
13
|
+
end
|
14
|
+
|
15
|
+
chart.data("other", data.map {|itm| itm[:value]}.inject(:+))
|
16
|
+
|
17
|
+
chart.to_blob
|
12
18
|
end
|
13
19
|
|
14
|
-
|
20
|
+
def self.make_bar_chart(title, data, additional_line_value = nil)
|
21
|
+
chart = Gruff::Bar.new
|
22
|
+
chart.minimum_value = 0
|
23
|
+
chart.maximum_value = data.first[:value]
|
24
|
+
chart.additional_line_values = [additional_line_value] unless additional_line_value.nil?
|
25
|
+
chart.title = title.to_s
|
26
|
+
data.each do |item|
|
27
|
+
chart.data(item[:label], item[:value])
|
28
|
+
end
|
29
|
+
|
30
|
+
chart.to_blob
|
31
|
+
end
|
15
32
|
|
16
|
-
|
33
|
+
def self.make_dependency_diagram(classes)
|
34
|
+
diagram = GraphViz.new(:G, :type => :digraph)
|
35
|
+
nodes = classes.map { |c| diagram.add_nodes(c.name) }
|
36
|
+
external_nodes = classes.map {|c| c.external_deps}.inject(:+).uniq.map {|n| diagram.add_nodes(n)}
|
37
|
+
classes.each do |klass|
|
38
|
+
classes.each do |other_klass|
|
39
|
+
if !klass.dependencies.index {|d| d == other_klass.name }.nil? || klass.dependencies.find_all {|d| other_klass.name =~ /^.*{0,}::#{d}$/}.size == 1
|
40
|
+
node, other_node = [klass, other_klass].map {|k| nodes.find {|n| n[:label].to_s.gsub('"', '') == k.name}}
|
41
|
+
diagram.add_edges(node, other_node)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
external_nodes.each {|n| n.set {|_n| _n.color = "blue"}}
|
45
|
+
external_nodes.each do |ext_node|
|
46
|
+
if klass.external_deps.include?(ext_node[:label].to_s.gsub('"', ''))
|
47
|
+
node = nodes.find {|n| n[:label].to_s.gsub('"', '') == klass.name}
|
48
|
+
diagram.add_edges(node, ext_node)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
diagram.output(:png => String)
|
53
|
+
end
|
17
54
|
end
|
55
|
+
end
|
56
|
+
|
57
|
+
#Monkeypatching additional line drawing
|
58
|
+
module Gruff
|
59
|
+
class Bar
|
60
|
+
def draw
|
61
|
+
@center_labels_over_point = (@labels.keys.length > @column_count ? true : false)
|
62
|
+
super
|
63
|
+
return unless @has_data
|
18
64
|
|
19
|
-
|
20
|
-
|
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])
|
65
|
+
draw_bars
|
66
|
+
draw_additional_line
|
26
67
|
end
|
68
|
+
def draw_bars
|
69
|
+
# Setup spacing.
|
70
|
+
#
|
71
|
+
# Columns sit side-by-side.
|
72
|
+
@bar_spacing ||= @spacing_factor # space between the bars
|
73
|
+
@bar_width = @graph_width / (@column_count * @data.length).to_f
|
74
|
+
padding = (@bar_width * (1 - @bar_spacing)) / 2
|
27
75
|
|
28
|
-
|
29
|
-
|
76
|
+
@d = @d.stroke_opacity 0.0
|
77
|
+
|
78
|
+
# Setup the BarConversion Object
|
79
|
+
conversion = Gruff::BarConversion.new()
|
80
|
+
conversion.graph_height = @graph_height
|
81
|
+
conversion.graph_top = @graph_top
|
30
82
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
83
|
+
# Set up the right mode [1,2,3] see BarConversion for further explanation
|
84
|
+
if @minimum_value >= 0 then
|
85
|
+
# all bars go from zero to positiv
|
86
|
+
conversion.mode = 1
|
87
|
+
else
|
88
|
+
# all bars go from 0 to negativ
|
89
|
+
if @maximum_value <= 0 then
|
90
|
+
conversion.mode = 2
|
91
|
+
else
|
92
|
+
# bars either go from zero to negativ or to positiv
|
93
|
+
conversion.mode = 3
|
94
|
+
conversion.spread = @spread
|
95
|
+
conversion.minimum_value = @minimum_value
|
96
|
+
conversion.zero = -@minimum_value/@spread
|
39
97
|
end
|
40
98
|
end
|
99
|
+
|
100
|
+
# iterate over all normalised data
|
101
|
+
@norm_data.each_with_index do |data_row, row_index|
|
102
|
+
|
103
|
+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
|
104
|
+
# Use incremented x and scaled y
|
105
|
+
# x
|
106
|
+
left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index))) + padding
|
107
|
+
right_x = left_x + @bar_width * @bar_spacing
|
108
|
+
# y
|
109
|
+
conv = []
|
110
|
+
conversion.get_left_y_right_y_scaled( data_point, conv )
|
111
|
+
|
112
|
+
# create new bar
|
113
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
114
|
+
@d = @d.rectangle(left_x, conv[0], right_x, conv[1])
|
115
|
+
|
116
|
+
# Calculate center based on bar_width and current row
|
117
|
+
label_center = @graph_left +
|
118
|
+
(@data.length * @bar_width * point_index) +
|
119
|
+
(@data.length * @bar_width / 2.0)
|
120
|
+
|
121
|
+
# Subtract half a bar width to center left if requested
|
122
|
+
draw_label(label_center - (@center_labels_over_point ? @bar_width / 2.0 : 0.0), point_index)
|
123
|
+
if @show_labels_for_bar_values
|
124
|
+
val = (@label_formatting || '%.2f') % @norm_data[row_index][3][point_index]
|
125
|
+
draw_value_label(left_x + (right_x - left_x)/2, conv[0]-30, val.commify, true)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Draw the last label if requested
|
131
|
+
draw_label(@graph_right, @column_count) if @center_labels_over_point
|
132
|
+
|
133
|
+
draw_additional_line
|
134
|
+
@d.draw(@base_image)
|
135
|
+
end
|
136
|
+
# Draws additional horizontal line
|
137
|
+
def draw_additional_line
|
138
|
+
@additional_line_colors << '#f61100'
|
139
|
+
@d = @d.stroke_opacity 100.0
|
140
|
+
i = 0
|
141
|
+
@additional_line_values.each do |value|
|
142
|
+
@increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
|
143
|
+
|
144
|
+
y = @graph_top + @graph_height - @increment_scaled
|
145
|
+
|
146
|
+
@d = @d.stroke(@additional_line_colors[i])
|
147
|
+
@d = @d.line(@graph_left, y, @graph_right, y)
|
148
|
+
|
149
|
+
|
150
|
+
@d.fill = @additional_line_colors[i]
|
151
|
+
@d.font = @font if @font
|
152
|
+
@d.stroke('transparent')
|
153
|
+
@d.pointsize = scale_fontsize(@marker_font_size)
|
154
|
+
@d.gravity = EastGravity
|
155
|
+
@d = @d.annotate_scaled( @base_image,
|
156
|
+
@graph_right - LABEL_MARGIN, 1.0,
|
157
|
+
0.0, y - (@marker_font_size/2.0),
|
158
|
+
label(value, value), @scale)
|
159
|
+
i += 1
|
160
|
+
end
|
161
|
+
|
162
|
+
@d = @d.stroke_antialias true
|
41
163
|
end
|
42
|
-
diagram.output(:png => String)
|
43
164
|
end
|
44
165
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reconn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mateusz Czarnecki
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-01-
|
11
|
+
date: 2015-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: vrlib
|