rubycritic-simplecov 4.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/CHANGELOG.md +290 -0
- data/CONTRIBUTING.md +93 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +259 -0
- data/ROADMAP.md +56 -0
- data/Rakefile +28 -0
- data/bin/rubycritic +10 -0
- data/lib/rubycritic/analysers/attributes.rb +29 -0
- data/lib/rubycritic/analysers/churn.rb +30 -0
- data/lib/rubycritic/analysers/complexity.rb +30 -0
- data/lib/rubycritic/analysers/coverage.rb +118 -0
- data/lib/rubycritic/analysers/helpers/ast_node.rb +76 -0
- data/lib/rubycritic/analysers/helpers/flay.rb +13 -0
- data/lib/rubycritic/analysers/helpers/flog.rb +17 -0
- data/lib/rubycritic/analysers/helpers/methods_counter.rb +25 -0
- data/lib/rubycritic/analysers/helpers/modules_locator.rb +42 -0
- data/lib/rubycritic/analysers/helpers/parser.rb +14 -0
- data/lib/rubycritic/analysers/helpers/reek.rb +11 -0
- data/lib/rubycritic/analysers/smells/flay.rb +79 -0
- data/lib/rubycritic/analysers/smells/flog.rb +69 -0
- data/lib/rubycritic/analysers/smells/reek.rb +53 -0
- data/lib/rubycritic/analysers_runner.rb +41 -0
- data/lib/rubycritic/analysis_summary.rb +40 -0
- data/lib/rubycritic/browser.rb +19 -0
- data/lib/rubycritic/cli/application.rb +34 -0
- data/lib/rubycritic/cli/options/argv.rb +137 -0
- data/lib/rubycritic/cli/options/file.rb +100 -0
- data/lib/rubycritic/cli/options.rb +33 -0
- data/lib/rubycritic/colorize.rb +17 -0
- data/lib/rubycritic/command_factory.rb +25 -0
- data/lib/rubycritic/commands/base.rb +18 -0
- data/lib/rubycritic/commands/ci.rb +14 -0
- data/lib/rubycritic/commands/compare.rb +106 -0
- data/lib/rubycritic/commands/default.rb +38 -0
- data/lib/rubycritic/commands/help.rb +18 -0
- data/lib/rubycritic/commands/status_reporter.rb +45 -0
- data/lib/rubycritic/commands/utils/build_number_file.rb +37 -0
- data/lib/rubycritic/commands/version.rb +16 -0
- data/lib/rubycritic/configuration.rb +69 -0
- data/lib/rubycritic/core/analysed_module.rb +93 -0
- data/lib/rubycritic/core/analysed_modules_collection.rb +95 -0
- data/lib/rubycritic/core/location.rb +47 -0
- data/lib/rubycritic/core/rating.rb +30 -0
- data/lib/rubycritic/core/smell.rb +84 -0
- data/lib/rubycritic/generators/console_report.rb +20 -0
- data/lib/rubycritic/generators/html/assets/fonts/FontAwesome.otf +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/Roboto-Medium.ttf +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/Roboto-Regular.ttf +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/fontawesome-webfont.eot +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/fontawesome-webfont.svg +2671 -0
- data/lib/rubycritic/generators/html/assets/fonts/fontawesome-webfont.ttf +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/fontawesome-webfont.woff +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/fontawesome-webfont.woff2 +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/glyphicons-halflings-regular.eot +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/glyphicons-halflings-regular.svg +288 -0
- data/lib/rubycritic/generators/html/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/glyphicons-halflings-regular.woff +0 -0
- data/lib/rubycritic/generators/html/assets/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/lib/rubycritic/generators/html/assets/images/logo.png +0 -0
- data/lib/rubycritic/generators/html/assets/javascripts/application.js +281 -0
- data/lib/rubycritic/generators/html/assets/javascripts/bootstrap.min.js +7 -0
- data/lib/rubycritic/generators/html/assets/javascripts/highcharts.src-4.0.1.js +17672 -0
- data/lib/rubycritic/generators/html/assets/javascripts/jquery.filtertable.min.js +13 -0
- data/lib/rubycritic/generators/html/assets/javascripts/jquery.min.js +4 -0
- data/lib/rubycritic/generators/html/assets/javascripts/jquery.scrollTo.min.js +7 -0
- data/lib/rubycritic/generators/html/assets/javascripts/jquery.tablesorter.js +1031 -0
- data/lib/rubycritic/generators/html/assets/javascripts/jquery.tablesorter.min.js +4 -0
- data/lib/rubycritic/generators/html/assets/javascripts/jquery.timeago.js +231 -0
- data/lib/rubycritic/generators/html/assets/javascripts/prettify.js +46 -0
- data/lib/rubycritic/generators/html/assets/stylesheets/application.css +570 -0
- data/lib/rubycritic/generators/html/assets/stylesheets/bootstrap.min.css +6 -0
- data/lib/rubycritic/generators/html/assets/stylesheets/font-awesome.min.css +4 -0
- data/lib/rubycritic/generators/html/assets/stylesheets/prettify.css +1 -0
- data/lib/rubycritic/generators/html/assets/stylesheets/prettify.custom_theme.css +69 -0
- data/lib/rubycritic/generators/html/base.rb +54 -0
- data/lib/rubycritic/generators/html/code_file.rb +51 -0
- data/lib/rubycritic/generators/html/code_index.rb +33 -0
- data/lib/rubycritic/generators/html/line.rb +37 -0
- data/lib/rubycritic/generators/html/overview.rb +38 -0
- data/lib/rubycritic/generators/html/simple_cov_index.rb +44 -0
- data/lib/rubycritic/generators/html/smells_index.rb +45 -0
- data/lib/rubycritic/generators/html/templates/code_file.html.erb +61 -0
- data/lib/rubycritic/generators/html/templates/code_index.html.erb +55 -0
- data/lib/rubycritic/generators/html/templates/layouts/application.html.erb +66 -0
- data/lib/rubycritic/generators/html/templates/line.html.erb +1 -0
- data/lib/rubycritic/generators/html/templates/overview.html.erb +68 -0
- data/lib/rubycritic/generators/html/templates/simple_cov_index.html.erb +44 -0
- data/lib/rubycritic/generators/html/templates/smells_index.html.erb +47 -0
- data/lib/rubycritic/generators/html/templates/smelly_line.html.erb +23 -0
- data/lib/rubycritic/generators/html/turbulence.rb +17 -0
- data/lib/rubycritic/generators/html/view_helpers.rb +58 -0
- data/lib/rubycritic/generators/html_report.rb +77 -0
- data/lib/rubycritic/generators/json/simple.rb +43 -0
- data/lib/rubycritic/generators/json_report.rb +26 -0
- data/lib/rubycritic/generators/lint_report.rb +30 -0
- data/lib/rubycritic/generators/text/lint.rb +41 -0
- data/lib/rubycritic/generators/text/list.rb +45 -0
- data/lib/rubycritic/generators/text/templates/lint.erb +3 -0
- data/lib/rubycritic/generators/text/templates/list.erb +12 -0
- data/lib/rubycritic/rake_task.rb +71 -0
- data/lib/rubycritic/reporter.rb +39 -0
- data/lib/rubycritic/revision_comparator.rb +45 -0
- data/lib/rubycritic/serializer.rb +32 -0
- data/lib/rubycritic/smells_status_setter.rb +18 -0
- data/lib/rubycritic/source_control_systems/base.rb +41 -0
- data/lib/rubycritic/source_control_systems/double.rb +19 -0
- data/lib/rubycritic/source_control_systems/git.rb +95 -0
- data/lib/rubycritic/source_control_systems/mercurial.rb +29 -0
- data/lib/rubycritic/source_control_systems/perforce.rb +116 -0
- data/lib/rubycritic/source_locator.rb +52 -0
- data/lib/rubycritic/version.rb +5 -0
- data/lib/rubycritic.rb +6 -0
- metadata +528 -0
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
These are more nice-to-haves than promises. We can always dream. But this is what we hope to improve in RubyCritic:
|
|
2
|
+
|
|
3
|
+
- [ ] Explain every single code smell. This includes the "Churn vs Complexity" scatter plot and other graphs that may be implemented.
|
|
4
|
+
|
|
5
|
+
- [ ] Provide suggestions to fix every code smell. Figure out how to present them in the UI.
|
|
6
|
+
|
|
7
|
+
- [ ] Improve how modules are graded. Each module is awarded a score, depending on:
|
|
8
|
+
|
|
9
|
+
* Its complexity, based off of this [post by Jake Scruggs](http://jakescruggs.blogspot.pt/2008/08/whats-good-flog-score.html), creator of the MetricFu gem. For every 25 points of complexity (as calculated by Flog), the score increases by 1.
|
|
10
|
+
|
|
11
|
+
* Its duplication mass, based off of observations of a few Code Climate repos. For every 25 points of mass (as calculated by Flay), the score increases by 1.
|
|
12
|
+
|
|
13
|
+
Finally, this score is translated to a grade like [this](https://github.com/whitesmith/rubycritic/blob/43005e7b76dd0c648c7715133e42afdd6ea9a065/lib/rubycritic/core/rating.rb), based off of a [Code Climate blog post](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/#value-objects).
|
|
14
|
+
|
|
15
|
+
- [ ] Implement a project rating, ala Code Climate.
|
|
16
|
+
|
|
17
|
+
- [ ] Explain ratings. What's the difference between an A and a B? Bryan Helmkamp, the creator of Code Climate, wrote [a great essay on the subject](https://gist.github.com/brynary/21369b5892525e1bd102). #63
|
|
18
|
+
|
|
19
|
+
- [ ] Explore alternative ratings. GPA and A-F grades are quite US centric. #50
|
|
20
|
+
|
|
21
|
+
- [ ] Make the gem configurable using a dotfile like .rubycritic.yml. #30
|
|
22
|
+
Here are some possible settings:
|
|
23
|
+
|
|
24
|
+
- [ ] Quiet mode. As of right now, any Ruby code that is unparsable will be reported three times (one time by Flog, another by Flay and another by Reek). Only Flog implements a quiet option, which means we have to implement that quiet option on Flay and on Reek before we can add it to RubyCritic. Or we could just do `$stderr = StringIO.new`... I wonder if that's really really smart or really really stupid.
|
|
25
|
+
|
|
26
|
+
- [ ] Verbose mode. #61
|
|
27
|
+
|
|
28
|
+
- [ ] Ignoring/excluding files. #11
|
|
29
|
+
|
|
30
|
+
- [ ] Allow configuring date range of Churn calculation. #37 Right now, they are limited to the last year. #39
|
|
31
|
+
|
|
32
|
+
- [ ] Highlight blocks of duplicated code instead of just the start of the block. This will probably require rewriting Flay with [parser](https://github.com/whitequark/parser) instead of ruby_parser.
|
|
33
|
+
|
|
34
|
+
- [ ] Integrate other gems, like:
|
|
35
|
+
|
|
36
|
+
- [ ] [Simplecov](https://github.com/colszowka/simplecov) to provide code coverage
|
|
37
|
+
|
|
38
|
+
- [ ] [Rubocop](https://github.com/bbatsov/rubocop/) to provide style issues
|
|
39
|
+
|
|
40
|
+
- [ ] [Brakeman](https://github.com/presidentbeef/brakeman) to provide security issues (Rails-only feature)
|
|
41
|
+
|
|
42
|
+
- [ ] [Rails Best Practices](https://github.com/railsbp/rails_best_practices) to provide Rails smells (Rails-only feature) #14
|
|
43
|
+
|
|
44
|
+
- [ ] [SandiMeter](https://github.com/makaroni4/sandi_meter) #15
|
|
45
|
+
|
|
46
|
+
- [ ] Improve UI.
|
|
47
|
+
|
|
48
|
+
- [ ] Make it beautiful.
|
|
49
|
+
|
|
50
|
+
- [ ] Figure out where the "suggestions to fix code smells" should be presented.
|
|
51
|
+
|
|
52
|
+
- [ ] Create some kind of toggle option between various types of issues. Just like we can toggle between "Smells" and "Coverage" in Code Climate:
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
Having an option to toggle between "Smells", "Security" (Brakeman) and "Style" (Rubocop) would be great. But that's already assuming we can integrate those gems into RubyCritic.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
require 'rubocop/rake_task'
|
|
6
|
+
require 'cucumber/rake/task'
|
|
7
|
+
require 'reek/rake/task'
|
|
8
|
+
require 'rubycritic/rake_task'
|
|
9
|
+
|
|
10
|
+
Rake::TestTask.new do |task|
|
|
11
|
+
task.libs.push 'lib'
|
|
12
|
+
task.libs.push 'test'
|
|
13
|
+
task.pattern = 'test/**/*_test.rb'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
|
17
|
+
t.cucumber_opts = 'features --format progress --color'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
RuboCop::RakeTask.new
|
|
21
|
+
|
|
22
|
+
Reek::Rake::Task.new
|
|
23
|
+
|
|
24
|
+
RubyCritic::RakeTask.new do |task|
|
|
25
|
+
task.paths = FileList['lib/**/*.rb']
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
task default: %i[test features reek rubocop]
|
data/bin/rubycritic
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Always look in the lib directory of this gem
|
|
5
|
+
# first when searching the load path
|
|
6
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
7
|
+
|
|
8
|
+
require 'rubycritic/cli/application'
|
|
9
|
+
|
|
10
|
+
exit RubyCritic::Cli::Application.new(ARGV).execute
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/analysers/helpers/methods_counter'
|
|
4
|
+
require 'rubycritic/analysers/helpers/modules_locator'
|
|
5
|
+
require 'rubycritic/colorize'
|
|
6
|
+
|
|
7
|
+
module RubyCritic
|
|
8
|
+
module Analyser
|
|
9
|
+
class Attributes
|
|
10
|
+
include Colorize
|
|
11
|
+
def initialize(analysed_modules)
|
|
12
|
+
@analysed_modules = analysed_modules
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
@analysed_modules.each do |analysed_module|
|
|
17
|
+
analysed_module.methods_count = MethodsCounter.new(analysed_module).count
|
|
18
|
+
analysed_module.name = ModulesLocator.new(analysed_module).first_name
|
|
19
|
+
print green '.'
|
|
20
|
+
end
|
|
21
|
+
puts ''
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
'attributes'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/colorize'
|
|
4
|
+
|
|
5
|
+
module RubyCritic
|
|
6
|
+
module Analyser
|
|
7
|
+
class Churn
|
|
8
|
+
include Colorize
|
|
9
|
+
attr_writer :source_control_system
|
|
10
|
+
|
|
11
|
+
def initialize(analysed_modules)
|
|
12
|
+
@analysed_modules = analysed_modules
|
|
13
|
+
@source_control_system = Config.source_control_system
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
@analysed_modules.each do |analysed_module|
|
|
18
|
+
analysed_module.churn = @source_control_system.revisions_count(analysed_module.path)
|
|
19
|
+
analysed_module.committed_at = @source_control_system.date_of_last_commit(analysed_module.path)
|
|
20
|
+
print green '.'
|
|
21
|
+
end
|
|
22
|
+
puts ''
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
'churn'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/analysers/helpers/flog'
|
|
4
|
+
require 'rubycritic/colorize'
|
|
5
|
+
|
|
6
|
+
module RubyCritic
|
|
7
|
+
module Analyser
|
|
8
|
+
class Complexity
|
|
9
|
+
include Colorize
|
|
10
|
+
def initialize(analysed_modules)
|
|
11
|
+
@flog = Flog.new
|
|
12
|
+
@analysed_modules = analysed_modules
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
@analysed_modules.each do |analysed_module|
|
|
17
|
+
@flog.reset
|
|
18
|
+
@flog.flog(analysed_module.path)
|
|
19
|
+
analysed_module.complexity = @flog.total_score.round(2)
|
|
20
|
+
print green '.'
|
|
21
|
+
end
|
|
22
|
+
puts ''
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
'complexity'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/colorize'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'simplecov'
|
|
6
|
+
|
|
7
|
+
module RubyCritic
|
|
8
|
+
module Analyser
|
|
9
|
+
class Coverage
|
|
10
|
+
include Colorize
|
|
11
|
+
|
|
12
|
+
def initialize(analysed_modules)
|
|
13
|
+
@analysed_modules = analysed_modules
|
|
14
|
+
@result = results.first
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
@analysed_modules.each do |analysed_module|
|
|
19
|
+
analysed_module.coverage = find_coverage_percentage(analysed_module)
|
|
20
|
+
print green '.'
|
|
21
|
+
end
|
|
22
|
+
puts ''
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
'simple_cov'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def find_coverage_percentage(analysed_module)
|
|
32
|
+
source_file = find_source_file(analysed_module)
|
|
33
|
+
|
|
34
|
+
return 0 unless source_file
|
|
35
|
+
|
|
36
|
+
source_file.covered_percent
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def find_source_file(analysed_module)
|
|
40
|
+
return unless @result
|
|
41
|
+
|
|
42
|
+
needle = File.join(SimpleCov.root, analysed_module.path)
|
|
43
|
+
|
|
44
|
+
@result.source_files.detect { |file| file.filename == needle }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The path to the .resultset.json cache file
|
|
48
|
+
def resultset_path
|
|
49
|
+
File.join(SimpleCov.coverage_path, '.resultset.json')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resultset_writelock
|
|
53
|
+
File.join(SimpleCov.coverage_path, '.resultset.json.lock')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Loads the cached resultset from JSON and returns it as a Hash,
|
|
57
|
+
# caching it for subsequent accesses.
|
|
58
|
+
def resultset
|
|
59
|
+
@resultset ||= begin
|
|
60
|
+
if (data = stored_data)
|
|
61
|
+
begin
|
|
62
|
+
JSON.parse(data) || {}
|
|
63
|
+
rescue JSON::ParserError => err
|
|
64
|
+
puts "Error: Loading .resultset.json: #{err.message}"
|
|
65
|
+
{}
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
{}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the contents of the resultset cache as a string or if the file is missing or empty nil
|
|
74
|
+
def stored_data
|
|
75
|
+
synchronize_resultset do
|
|
76
|
+
return unless File.exist?(resultset_path)
|
|
77
|
+
|
|
78
|
+
return unless (data = File.read(resultset_path))
|
|
79
|
+
|
|
80
|
+
return if data.length < 2
|
|
81
|
+
|
|
82
|
+
data
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Ensure only one process is reading or writing the resultset at any
|
|
87
|
+
# given time
|
|
88
|
+
def synchronize_resultset
|
|
89
|
+
# make it reentrant
|
|
90
|
+
return yield if @resultset_locked == true
|
|
91
|
+
|
|
92
|
+
return yield unless File.exist?(resultset_writelock)
|
|
93
|
+
|
|
94
|
+
begin
|
|
95
|
+
@resultset_locked = true
|
|
96
|
+
File.open(resultset_writelock, 'w+') do |file|
|
|
97
|
+
file.flock(File::LOCK_EX)
|
|
98
|
+
yield
|
|
99
|
+
end
|
|
100
|
+
ensure
|
|
101
|
+
@resultset_locked = false
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Gets the resultset hash and re-creates all included instances
|
|
106
|
+
# of SimpleCov::Result from that.
|
|
107
|
+
# All results that are above the SimpleCov.merge_timeout will be
|
|
108
|
+
# dropped. Returns an array of SimpleCov::Result items.
|
|
109
|
+
def results
|
|
110
|
+
array = []
|
|
111
|
+
resultset.each do |command_name, data|
|
|
112
|
+
array << ::SimpleCov::Result.from_hash(command_name => data)
|
|
113
|
+
end
|
|
114
|
+
array
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parser
|
|
4
|
+
module AST
|
|
5
|
+
class Node
|
|
6
|
+
MODULE_TYPES = %i[module class].freeze
|
|
7
|
+
|
|
8
|
+
def count_nodes_of_type(*types)
|
|
9
|
+
count = 0
|
|
10
|
+
recursive_children do |child|
|
|
11
|
+
count += 1 if types.include?(child.type)
|
|
12
|
+
end
|
|
13
|
+
count
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def recursive_children
|
|
17
|
+
children.each do |child|
|
|
18
|
+
next unless child.is_a?(Parser::AST::Node)
|
|
19
|
+
|
|
20
|
+
yield child
|
|
21
|
+
child.recursive_children { |grand_child| yield grand_child }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def module_names
|
|
26
|
+
ast_node_children = children.select do |child|
|
|
27
|
+
child.is_a?(Parser::AST::Node)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
children_modules = ast_node_children.flat_map(&:module_names)
|
|
31
|
+
|
|
32
|
+
if MODULE_TYPES.include?(type)
|
|
33
|
+
module_names_with_children children_modules
|
|
34
|
+
else
|
|
35
|
+
children_modules
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def module_names_with_children(children_modules)
|
|
42
|
+
if children_modules.empty?
|
|
43
|
+
[module_name]
|
|
44
|
+
else
|
|
45
|
+
children_modules.map do |children_module|
|
|
46
|
+
"#{module_name}::#{children_module}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def module_name
|
|
52
|
+
name_segments = []
|
|
53
|
+
current_node = children[0]
|
|
54
|
+
while current_node
|
|
55
|
+
name_segments.unshift(current_node.children[1])
|
|
56
|
+
current_node = current_node.children[0]
|
|
57
|
+
end
|
|
58
|
+
name_segments.join('::')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
module RubyCritic
|
|
65
|
+
module AST
|
|
66
|
+
class EmptyNode
|
|
67
|
+
def count_nodes_of_type(*)
|
|
68
|
+
0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def module_names
|
|
72
|
+
[]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/analysers/helpers/parser'
|
|
4
|
+
|
|
5
|
+
module RubyCritic
|
|
6
|
+
class MethodsCounter
|
|
7
|
+
def initialize(analysed_module)
|
|
8
|
+
@analysed_module = analysed_module
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def count
|
|
12
|
+
node.count_nodes_of_type(:def, :defs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def node
|
|
18
|
+
Parser.parse(content)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def content
|
|
22
|
+
File.read(@analysed_module.path)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/analysers/helpers/parser'
|
|
4
|
+
|
|
5
|
+
module RubyCritic
|
|
6
|
+
class ModulesLocator
|
|
7
|
+
def initialize(analysed_module)
|
|
8
|
+
@analysed_module = analysed_module
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def first_name
|
|
12
|
+
names.first
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def names
|
|
16
|
+
names = node.module_names
|
|
17
|
+
if names.empty?
|
|
18
|
+
name_from_path
|
|
19
|
+
else
|
|
20
|
+
names
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def node
|
|
27
|
+
Parser.parse(content)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def content
|
|
31
|
+
File.read(@analysed_module.path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def name_from_path
|
|
35
|
+
[file_name.split('_').map(&:capitalize).join]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def file_name
|
|
39
|
+
@analysed_module.pathname.basename.sub_ext('').to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parser/current'
|
|
4
|
+
require 'rubycritic/analysers/helpers/ast_node'
|
|
5
|
+
|
|
6
|
+
module RubyCritic
|
|
7
|
+
module Parser
|
|
8
|
+
def self.parse(content)
|
|
9
|
+
::Parser::CurrentRuby.parse(content) || AST::EmptyNode.new
|
|
10
|
+
rescue ::Parser::SyntaxError
|
|
11
|
+
AST::EmptyNode.new
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/analysers/helpers/flay'
|
|
4
|
+
require 'rubycritic/core/smell'
|
|
5
|
+
require 'rubycritic/colorize'
|
|
6
|
+
|
|
7
|
+
module RubyCritic
|
|
8
|
+
module Analyser
|
|
9
|
+
class FlaySmells
|
|
10
|
+
include Colorize
|
|
11
|
+
def initialize(analysed_modules)
|
|
12
|
+
@analysed_modules = paths_to_analysed_modules(analysed_modules)
|
|
13
|
+
@flay = Flay.new(@analysed_modules.keys)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
@flay.hashes.each do |structural_hash, nodes|
|
|
18
|
+
analyze_modules(structural_hash, nodes)
|
|
19
|
+
print green '.'
|
|
20
|
+
end
|
|
21
|
+
puts ''
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
'flay smells'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def paths_to_analysed_modules(analysed_modules)
|
|
31
|
+
paths = {}
|
|
32
|
+
analysed_modules.each do |analysed_module|
|
|
33
|
+
paths[analysed_module.path] = analysed_module
|
|
34
|
+
end
|
|
35
|
+
paths
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_smell(structural_hash, nodes)
|
|
39
|
+
mass = @flay.masses[structural_hash]
|
|
40
|
+
Smell.new(
|
|
41
|
+
locations: smell_locations(nodes),
|
|
42
|
+
context: similarity(structural_hash),
|
|
43
|
+
message: "found in #{nodes.size} nodes",
|
|
44
|
+
score: mass,
|
|
45
|
+
type: 'DuplicateCode',
|
|
46
|
+
analyser: 'flay',
|
|
47
|
+
cost: cost(mass)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def smell_locations(nodes)
|
|
52
|
+
nodes.map do |node|
|
|
53
|
+
Location.new(node.file, node.line)
|
|
54
|
+
end.sort
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def similarity(structural_hash)
|
|
58
|
+
if @flay.identical[structural_hash]
|
|
59
|
+
'Identical code'
|
|
60
|
+
else
|
|
61
|
+
'Similar code'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cost(mass)
|
|
66
|
+
mass / 25
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def analyze_modules(structural_hash, nodes)
|
|
70
|
+
nodes.map(&:file).uniq.each do |file|
|
|
71
|
+
@analysed_modules[file].smells << create_smell(structural_hash, nodes)
|
|
72
|
+
end
|
|
73
|
+
nodes.each do |node|
|
|
74
|
+
@analysed_modules[node.file].duplication += node.mass
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycritic/analysers/helpers/flog'
|
|
4
|
+
require 'rubycritic/core/smell'
|
|
5
|
+
require 'rubycritic/colorize'
|
|
6
|
+
|
|
7
|
+
module RubyCritic
|
|
8
|
+
module Analyser
|
|
9
|
+
class FlogSmells
|
|
10
|
+
include Colorize
|
|
11
|
+
HIGH_COMPLEXITY_SCORE_THRESHOLD = 25
|
|
12
|
+
VERY_HIGH_COMPLEXITY_SCORE_THRESHOLD = 60
|
|
13
|
+
|
|
14
|
+
def initialize(analysed_modules)
|
|
15
|
+
@flog = Flog.new
|
|
16
|
+
@analysed_modules = analysed_modules
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
@analysed_modules.each do |analysed_module|
|
|
21
|
+
add_smells_to(analysed_module)
|
|
22
|
+
print green '.'
|
|
23
|
+
end
|
|
24
|
+
puts ''
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
'flog smells'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def add_smells_to(analysed_module)
|
|
34
|
+
@flog.reset
|
|
35
|
+
@flog.flog(analysed_module.path)
|
|
36
|
+
@flog.each_by_score do |class_method, original_score|
|
|
37
|
+
score = original_score.round
|
|
38
|
+
analysed_module.smells << create_smell(class_method, score) if score >= HIGH_COMPLEXITY_SCORE_THRESHOLD
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create_smell(context, score)
|
|
43
|
+
Smell.new(
|
|
44
|
+
locations: smell_locations(context),
|
|
45
|
+
context: context,
|
|
46
|
+
message: "has a flog score of #{score}",
|
|
47
|
+
score: score,
|
|
48
|
+
type: type(score),
|
|
49
|
+
analyser: 'flog',
|
|
50
|
+
cost: 0
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def smell_locations(context)
|
|
55
|
+
line = @flog.method_locations[context]
|
|
56
|
+
file_path, file_line = line.split(':')
|
|
57
|
+
[Location.new(file_path, file_line)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def type(score)
|
|
61
|
+
if score >= VERY_HIGH_COMPLEXITY_SCORE_THRESHOLD
|
|
62
|
+
'VeryHighComplexity'
|
|
63
|
+
else
|
|
64
|
+
'HighComplexity'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|