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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 13aa9f0063ed8270baa802db3b86f308b2aa4066
4
+ data.tar.gz: 1b7df1c4764ce1a90b64aab61036819d6e6d67c3
5
+ SHA512:
6
+ metadata.gz: 97c1778b30eed241da1babbd81be892ffc32dadc4dfc451864bacc26ecc9dc600c841e8c7752e6b1ba229bd707c3cb58a52ebbf3aeb92c29e419f8ef6d8d3943
7
+ data.tar.gz: 78d10bae27845cd07162bb3470753c575811cba9bbb3a51b559bac7a9f8911de147d17c376538118fd075ceb5c8531fdaa6292a70b6738658665629c6f1c782b
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .vr_settings.yaml
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rubycode_analyzer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Reconn
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'reconn'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install reconn
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( http://github.com/<my-github-username>/recon/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new
data/bin/reconn ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'vrlib'
4
+
5
+ #make program output in real time so errors visible in VR.
6
+ STDOUT.sync = true
7
+ STDERR.sync = true
8
+
9
+ #everything in these directories will be included
10
+ my_path = File.expand_path(File.dirname(__FILE__))
11
+ require_all Dir.glob(my_path + "/../lib/reconn/**/*.rb")
12
+
13
+ View.new.show
data/lib/reconn.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'reconn/version'
2
+ require 'reconn/analyzer'
3
+
4
+ module Reconn
5
+ end
@@ -0,0 +1,201 @@
1
+ # @author Mateusz Czarnecki <mateusz.czarnecki92@gmail.com>
2
+
3
+ require 'ruby_parser'
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
+
9
+ module Analyzer
10
+ class Analyzer < MethodBasedSexpProcessor
11
+
12
+ MAX_METHOD_LENGTH = 10
13
+ MAX_COMPLEXITY = 6
14
+
15
+ def initialize
16
+ super()
17
+ self.auto_shift_type = false
18
+ @classes = []
19
+ @methods = []
20
+ @smells = []
21
+ @current_class = Class.new(:none)
22
+ end
23
+
24
+ # Analyzes all the ruby files in the given directory and its subdirectories
25
+ # @param dir [String] path to the directory
26
+ # @return [Array(Array<Class>, Array<Method>, Array<CodeSmell>)] found classes,
27
+ # methods and code smells
28
+ def analyze(dir)
29
+ parser = RubyParser.new
30
+ paths = ProjectScanner.scan(dir)
31
+ paths.each do |path|
32
+ @current_path = path
33
+ ast = parser.process(File.binread(path), path)
34
+ process ast
35
+ @current_class = Class.new(:none)
36
+ end
37
+
38
+ merged_classes = merge_duplicate_classes
39
+ @classes = merged_classes unless merged_classes.nil?
40
+
41
+ @classes.each do |klass|
42
+ klass.lines = count_lines_in_class(klass)
43
+ klass.complexity = count_complexity_in_class(klass)
44
+ end
45
+ prune_dependencies
46
+
47
+ # Deletes empty classes
48
+ @classes.delete(Class.new(:none))
49
+
50
+ @smells = find_code_smells
51
+
52
+ return @classes, @methods, @smells
53
+ end
54
+
55
+ #########################################
56
+ # Process methods:
57
+
58
+ def process_class(exp)
59
+ exp.shift
60
+ in_klass(exp.shift) do
61
+ @current_class = Class.new(klass_name)
62
+ @classes << @current_class
63
+ process_until_empty exp
64
+ end
65
+
66
+ @current_class = Class.new(:none)
67
+ s()
68
+ end
69
+
70
+ def process_defn(exp)
71
+ exp.shift
72
+ method_name = exp.shift.to_s
73
+ lines = count_lines_in_method(method_name)
74
+ @current_method = Method.new(method_name, @current_class.name, lines)
75
+ exp.shift
76
+ process_until_empty exp
77
+
78
+ @methods << @current_method
79
+ @current_class.add_method(@current_method)
80
+ @current_method = Method.new(:none)
81
+ s()
82
+ end
83
+
84
+ def process_defs(exp)
85
+ exp.shift
86
+ process_defn(exp)
87
+ end
88
+
89
+ def process_colon2(exp)
90
+ exp.shift
91
+ name = exp.flatten
92
+ name.delete :colon2
93
+ name.delete :const
94
+ name = name.join("::")
95
+ @current_class.add_dependency(name)
96
+ exp.shift
97
+ process_until_empty exp
98
+
99
+ s()
100
+ end
101
+
102
+ def process_const(exp)
103
+ exp.shift
104
+ name = exp.shift.to_s
105
+ @current_class.add_dependency(name)
106
+ exp.shift
107
+ process_until_empty exp
108
+
109
+ s()
110
+ end
111
+
112
+ def process_if(exp)
113
+ exp.shift
114
+ process_until_empty exp
115
+
116
+ @current_method.incr_complexity unless @current_method.nil?
117
+ s()
118
+ end
119
+ alias process_for process_if
120
+ alias process_when process_if
121
+ alias process_until process_if
122
+ alias process_while process_if
123
+ alias process_rescue process_if
124
+ alias process_and process_if
125
+ alias process_or process_if
126
+
127
+ ########################################
128
+
129
+ #Counts lines of code in a method
130
+ #
131
+ #@param method_name [String] the name of the method
132
+ #@return [Integer] lines of code count
133
+ def count_lines_in_method(method_name)
134
+ method_name = method_name.gsub(/[\.\|\(\)\[\]\{\}\+\\\^\$\*\?]/) {|match| '\\' + match}
135
+ flag = false
136
+ lines = []
137
+ File.foreach(@current_path) do |line|
138
+ break if line =~ /def/ && flag
139
+ lines << line if flag && line.strip != '' && line.strip[0] != '#'
140
+ flag = true if line =~ /def #{method_name}/ || line =~ /def self.#{method_name}/
141
+ end
142
+
143
+ lines.size
144
+ end
145
+
146
+ #Counts lines of code in a class (sums LOC of methods)
147
+ #
148
+ #@param klass [Class] the class
149
+ #@return [Integer] lines of code count
150
+ def count_lines_in_class(klass)
151
+ lines = klass.methods.map {|method| method.lines}.inject(:+)
152
+ lines.nil? ? 0 : lines
153
+ end
154
+
155
+ def count_complexity_in_class(klass)
156
+ complexity = klass.methods.map {|method| method.complexity}.inject(:+)
157
+ complexity.nil? ? 0 : complexity
158
+ end
159
+
160
+ #Deletes dependencies which are not classes within analyzed project
161
+ def prune_dependencies
162
+ class_names = @classes.map {|klass| klass.name}
163
+ @classes.each do |klass|
164
+ klass.dependencies = klass.dependencies.map do |dep|
165
+ dep_split = dep.split('::')
166
+ klass_split = klass.name.split('::')
167
+ klass_split.pop(dep_split.size)
168
+ (klass_split + dep_split).join('::')
169
+ end
170
+ klass.dependencies = klass.dependencies.uniq.keep_if {|dep| dep != klass.name && class_names.include?(dep)}
171
+ end
172
+ end
173
+
174
+ def merge_duplicate_classes
175
+ duplicates = @classes.group_by {|c| c.name}.select {|k, v| v.size > 1}.values
176
+ if !duplicates.empty?
177
+ merged_dups = []
178
+ duplicates.each do |dup|
179
+ merged_dups << dup.inject(:+)
180
+ end
181
+ @classes.uniq {|c| c.name}.each do |klass|
182
+ klass = merged_dups.find {|d| d == klass} if merged_dups.include?(klass)
183
+ end
184
+ end
185
+ end
186
+
187
+ def find_code_smells
188
+ code_smells = []
189
+ @methods.each do |method|
190
+ if method.lines > MAX_METHOD_LENGTH
191
+ code_smells << CodeSmell.new(:too_big_method, method.class_name, method.name)
192
+ end
193
+ if method.complexity > MAX_COMPLEXITY
194
+ code_smells << CodeSmell.new(:too_complex_method, method.class_name, method.name)
195
+ end
196
+ end
197
+ code_smells
198
+ end
199
+
200
+ end
201
+ end
@@ -0,0 +1,16 @@
1
+ module Analyzer
2
+ class CodeSmell
3
+ attr_reader :type, :class_name, :method_name
4
+
5
+ def initialize(type, class_name = :none, method_name)
6
+ @type = type
7
+ @class_name = class_name
8
+ @method_name = method_name
9
+ end
10
+
11
+ def to_s
12
+ "Smell: #{@type.to_s} in Class: #{@class_name.to_s} "\
13
+ "Method: #{@method_name.to_s}"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,48 @@
1
+ module Analyzer
2
+ # Represents a class in the project
3
+ class Class
4
+ attr_reader :name, :methods
5
+ attr_accessor :lines, :complexity, :dependencies
6
+
7
+ include Comparable
8
+
9
+ def ==(other)
10
+ name == other.name
11
+ end
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ @dependencies = []
16
+ @methods = []
17
+ @lines = 0
18
+ @complexity = 0
19
+ end
20
+
21
+ def add_dependency(class_name)
22
+ @dependencies << class_name.to_s
23
+ end
24
+
25
+ def add_method(method_name)
26
+ @methods << method_name
27
+ end
28
+
29
+ def methods_number
30
+ methods.size
31
+ end
32
+
33
+ def +(other)
34
+ other.methods.each do |method|
35
+ if !@methods.index(method)
36
+ @methods << method
37
+ end
38
+ end
39
+ @dependencies += other.dependencies
40
+ self
41
+ end
42
+
43
+ def to_s
44
+ name.to_s
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ module Analyzer
2
+ # Represents a method in the project
3
+ class Method
4
+ attr_reader :name, :class_name, :lines, :complexity
5
+
6
+ include Comparable
7
+
8
+ def ==(other)
9
+ name == other.name && class_name == other.class_name
10
+ end
11
+
12
+ def initialize(name, class_name = :none, lines = 0)
13
+ @name = name
14
+ @class_name = class_name
15
+ @lines = lines
16
+ @complexity = 1
17
+ end
18
+
19
+ def to_s
20
+ name.to_s
21
+ end
22
+
23
+ def incr_complexity
24
+ @complexity += 1
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require 'find'
2
+
3
+ class ProjectScanner
4
+ # Scans the given directory and all its subdirectories for ruby files
5
+ #
6
+ # @param proj_path [String] path to the project directory
7
+ # @return [Array<String>] paths to the ruby files
8
+ # @raise [InvalidPathException] if it can't open the directory
9
+ def self.scan(proj_path)
10
+ paths = []
11
+ begin
12
+ Find.find(proj_path) do |path|
13
+ if FileTest.directory?(path)
14
+ if File.basename(path)[0] == '.'
15
+ Find.prune
16
+ end
17
+ end
18
+ if File.extname(path) == '.rb'
19
+ paths << path
20
+ end
21
+ end
22
+ rescue
23
+ raise InvalidPathException, "Can't open the directory"
24
+ end
25
+
26
+ paths
27
+ end
28
+ end