├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .mdlrc ├── .reek.yml ├── .rubocop.yml ├── .solargraph.yml ├── .yardopts ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── ROADMAP.md ├── Rakefile ├── bin └── rubycritic ├── docs ├── building-own-code-climate.md ├── core-metrics.md ├── formatters.md └── jenkins-pr-reviews.md ├── features ├── command_line_interface │ ├── minimum_score.feature │ └── options.feature ├── rake_task.feature ├── step_definitions │ ├── rake_task_steps.rb │ ├── rubycritic_steps.rb │ └── sample_file_steps.rb └── support │ └── env.rb ├── gemfiles ├── simplecov_0.17.gemfile ├── simplecov_0.17.gemfile.lock ├── simplecov_0.18.gemfile ├── simplecov_0.18.gemfile.lock ├── simplecov_0.19.gemfile └── simplecov_0.19.gemfile.lock ├── images ├── churn-vs-complexity.png ├── code.png ├── logo.png ├── logo.svg ├── overview.png ├── rating.png ├── reek.png ├── smell-details.png ├── smells.png └── whitesmith.png ├── lib ├── rubycritic.rb └── rubycritic │ ├── analysers │ ├── attributes.rb │ ├── churn.rb │ ├── complexity.rb │ ├── coverage.rb │ ├── helpers │ │ ├── ast_node.rb │ │ ├── flay.rb │ │ ├── flog.rb │ │ ├── methods_counter.rb │ │ ├── modules_locator.rb │ │ ├── parser.rb │ │ └── reek.rb │ └── smells │ │ ├── flay.rb │ │ ├── flog.rb │ │ └── reek.rb │ ├── analysers_runner.rb │ ├── analysis_summary.rb │ ├── browser.rb │ ├── cli │ ├── application.rb │ ├── options.rb │ └── options │ │ ├── argv.rb │ │ └── file.rb │ ├── colorize.rb │ ├── command_factory.rb │ ├── commands │ ├── base.rb │ ├── ci.rb │ ├── compare.rb │ ├── default.rb │ ├── help.rb │ ├── status_reporter.rb │ ├── utils │ │ └── build_number_file.rb │ └── version.rb │ ├── configuration.rb │ ├── core │ ├── analysed_module.rb │ ├── analysed_modules_collection.rb │ ├── location.rb │ ├── rating.rb │ └── smell.rb │ ├── generators │ ├── console_report.rb │ ├── html │ │ ├── assets │ │ │ ├── fonts │ │ │ │ ├── Roboto-Medium.ttf │ │ │ │ └── Roboto-Regular.ttf │ │ │ ├── images │ │ │ │ └── logo.png │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ ├── stylesheets │ │ │ │ └── application.css │ │ │ └── vendor │ │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── fontawesome-webfont.woff2 │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ │ ├── javascripts │ │ │ │ ├── bootstrap.min.js │ │ │ │ ├── highcharts.src-4.0.1.js │ │ │ │ ├── jquery.filtertable.min.js │ │ │ │ ├── jquery.min.js │ │ │ │ ├── jquery.scrollTo.min.js │ │ │ │ ├── jquery.tablesorter.js │ │ │ │ ├── jquery.tablesorter.min.js │ │ │ │ ├── jquery.timeago.js │ │ │ │ └── prettify.js │ │ │ │ └── stylesheets │ │ │ │ ├── bootstrap.min.css │ │ │ │ ├── font-awesome.min.css │ │ │ │ ├── prettify.css │ │ │ │ └── prettify.custom_theme.css │ │ ├── base.rb │ │ ├── code_file.rb │ │ ├── code_index.rb │ │ ├── line.rb │ │ ├── overview.rb │ │ ├── simple_cov_index.rb │ │ ├── smells_index.rb │ │ ├── templates │ │ │ ├── code_file.html.erb │ │ │ ├── code_index.html.erb │ │ │ ├── layouts │ │ │ │ └── application.html.erb │ │ │ ├── line.html.erb │ │ │ ├── overview.html.erb │ │ │ ├── simple_cov_index.html.erb │ │ │ ├── smells_index.html.erb │ │ │ └── smelly_line.html.erb │ │ ├── turbulence.rb │ │ └── view_helpers.rb │ ├── html_report.rb │ ├── json │ │ └── simple.rb │ ├── json_report.rb │ ├── lint_report.rb │ └── text │ │ ├── lint.rb │ │ ├── list.rb │ │ └── templates │ │ ├── lint.erb │ │ └── list.erb │ ├── rake_task.rb │ ├── reporter.rb │ ├── revision_comparator.rb │ ├── serializer.rb │ ├── smells_status_setter.rb │ ├── source_control_systems │ ├── base.rb │ ├── double.rb │ ├── git.rb │ ├── git │ │ └── churn.rb │ ├── mercurial.rb │ └── perforce.rb │ ├── source_locator.rb │ └── version.rb ├── pull_request_template.md ├── rubycritic.gemspec ├── test ├── analysers_test_helper.rb ├── fakefs_helper.rb ├── lib │ └── rubycritic │ │ ├── analysers │ │ ├── churn_test.rb │ │ ├── complexity_test.rb │ │ ├── coverage_test.rb │ │ ├── helpers │ │ │ ├── methods_counter_test.rb │ │ │ └── modules_locator_test.rb │ │ └── smells │ │ │ ├── flay_test.rb │ │ │ ├── flog_test.rb │ │ │ └── reek_test.rb │ │ ├── analysis_summary_test.rb │ │ ├── browser_test.rb │ │ ├── commands │ │ ├── compare_test.rb │ │ └── status_reporter_test.rb │ │ ├── configuration_test.rb │ │ ├── core │ │ ├── analysed_module_test.rb │ │ ├── analysed_modules_collection_test.rb │ │ ├── location_test.rb │ │ ├── smell_test.rb │ │ └── smells_array_test.rb │ │ ├── generators │ │ ├── console_report_test.rb │ │ ├── html_report_test.rb │ │ ├── json_report_test.rb │ │ ├── lint_report_test.rb │ │ ├── turbulence_test.rb │ │ └── view_helpers_test.rb │ │ ├── reporter_test.rb │ │ ├── revision_comparator_test.rb │ │ ├── smells_status_setter_test.rb │ │ ├── source_control_systems │ │ ├── base_test.rb │ │ ├── double_test.rb │ │ ├── git │ │ │ └── churn_test.rb │ │ ├── git_test.rb │ │ ├── interfaces │ │ │ ├── basic.rb │ │ │ └── time_travel.rb │ │ ├── mercurial_test.rb │ │ └── perforce_test.rb │ │ ├── source_locator_test.rb │ │ └── version_test.rb ├── samples │ ├── base_branch_file.rb │ ├── compare_file.rb │ ├── coverage_sample │ │ ├── 0.18 │ │ │ └── .resultset.json │ │ └── 0.21 │ │ │ └── .resultset.json │ ├── dummy_formatter.rb │ ├── empty.rb │ ├── feature_branch_file.rb │ ├── flay │ │ ├── .flayignore │ │ ├── smelly.rb │ │ └── smelly2.rb │ ├── flog │ │ ├── complex.rb │ │ └── smelly.rb │ ├── location │ │ ├── dir1 │ │ │ └── file1.rb │ │ ├── file0.rb │ │ ├── file0_symlink.rb │ │ ├── file_with_different_extension.py │ │ ├── file_with_no_extension │ │ ├── file_with_ruby_shebang │ │ └── ruby_file_different_extension.foo │ ├── methods_count.rb │ ├── module_names.rb │ ├── no_methods.rb │ ├── reek │ │ ├── not_smelly.rb │ │ └── smelly.rb │ └── simple_cov_index.html └── test_helper.rb └── travis_scripts ├── before_script.sh └── script.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://EditorConfig.org 3 | 4 | root = true 5 | [*] 6 | charset = utf-8 7 | indent_size = 2 8 | end_of_line = lf 9 | indent_style = space 10 | max_line_length = 180 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | test/samples/report.json 18 | test/samples/lint.txt 19 | test/samples/overview.html 20 | test/samples/code_index.html 21 | test/samples/smells_index.html 22 | test/samples/test 23 | test/samples/assets 24 | tmp 25 | .idea/ 26 | .ruby-gemset 27 | .ruby-version 28 | .DS_Store 29 | .byebug_history 30 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | git_recurse true 2 | rules "~MD004,~MD006,~MD013,~MD014,~MD025,~MD029,~MD032,~MD033,~MD038" 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-minitest 3 | - rubocop-performance 4 | - rubocop-rake 5 | 6 | AllCops: 7 | DisplayCopNames: true 8 | NewCops: enable 9 | Exclude: 10 | - 'test/samples/**/*' 11 | - 'tmp/**/*' 12 | - 'vendor/**/*' 13 | - 'gemfiles/*' 14 | TargetRubyVersion: 3.1 15 | 16 | Metrics/BlockLength: 17 | Enabled: false 18 | 19 | Metrics/MethodLength: 20 | Exclude: 21 | - "lib/rubycritic/configuration.rb" 22 | 23 | Layout/LineLength: 24 | Max: 120 25 | 26 | Style/Documentation: 27 | Enabled: false 28 | 29 | Style/HashSyntax: 30 | EnforcedShorthandSyntax: either 31 | 32 | Security/MarshalLoad: 33 | Enabled: false 34 | Include: 35 | - 'lib/rubycritic/serializer.rb' 36 | 37 | Style/RedundantFreeze: 38 | Enabled: false 39 | Include: 40 | - 'lib/rubycritic/core/smell.rb' 41 | - 'lib/rubycritic/generators/json/simple.rb' 42 | - 'lib/rubycritic/revision_comparator.rb' 43 | - 'lib/rubycritic/source_control_systems/perforce.rb' 44 | - 'lib/rubycritic/source_locator.rb' 45 | - 'lib/rubycritic/version.rb' 46 | 47 | Layout/BlockAlignment: 48 | Enabled: false 49 | Exclude: 50 | - 'features/step_definitions/rake_task_steps.rb' 51 | 52 | Naming/RescuedExceptionsVariableName: 53 | Exclude: 54 | - 'lib/rubycritic/analysers/coverage.rb' 55 | - 'lib/rubycritic/cli/application.rb' 56 | - 'lib/rubycritic/reporter.rb' 57 | 58 | Lint/EmptyClass: 59 | Exclude: 60 | - 'test/lib/rubycritic/reporter_test.rb' 61 | 62 | Lint/ConstantDefinitionInBlock: 63 | Exclude: 64 | - 'test/lib/rubycritic/reporter_test.rb' 65 | 66 | Style/OpenStructUse: 67 | Exclude: 68 | - 'test/lib/rubycritic/generators/turbulence_test.rb' 69 | - 'test/lib/rubycritic/generators/console_report_test.rb' 70 | - 'test/lib/rubycritic/core/analysed_module_test.rb' 71 | - 'test/analysers_test_helper.rb' 72 | 73 | Lint/MissingSuper: 74 | Exclude: 75 | - 'test/analysers_test_helper.rb' 76 | - 'lib/rubycritic/generators/html/overview.rb' 77 | - 'lib/rubycritic/generators/html/simple_cov_index.rb' 78 | - 'lib/rubycritic/generators/html/smells_index.rb' 79 | - 'lib/rubycritic/rake_task.rb' 80 | - 'lib/rubycritic/generators/html/code_file.rb' 81 | - 'lib/rubycritic/generators/html/code_index.rb' 82 | - 'lib/rubycritic/generators/html/line.rb' 83 | 84 | Gemspec/DevelopmentDependencies: 85 | Exclude: 86 | - 'rubycritic.gemspec' 87 | 88 | Lint/StructNewOverride: 89 | Exclude: 90 | - 'lib/rubycritic/source_control_systems/git/churn.rb' 91 | 92 | Metrics/AbcSize: 93 | Exclude: 94 | - 'lib/rubycritic/configuration.rb' 95 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "./**/*.rb" 3 | exclude: 4 | - ".bundle/**/*" 5 | require: [] 6 | domains: [] 7 | reporters: 8 | - rubocop 9 | - require_not_found 10 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --use-cache 3 | --asset images 4 | lib/**/*.rb - 5 | README.md LICENSE.txt CHANGELOG.md 6 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'simplecov-0.17' do 4 | gem 'simplecov', '0.17' 5 | end 6 | 7 | appraise 'simplecov-0.18' do 8 | gem 'simplecov', '0.18' 9 | end 10 | 11 | appraise 'simplecov-0.19' do 12 | gem 'simplecov', '0.19' 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in rubycritic.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Guilherme Simoes 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. 23 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 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 | - [x] 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 | - [x] [Simplecov](https://github.com/colszowka/simplecov) to provide code coverage. #319 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 | ![Code Climate Toggle Option](https://camo.githubusercontent.com/d97fc62dae6ebef1f35bda91942d4a6bacc445b2/687474703a2f2f626c6f672e636f6465636c696d6174652e636f6d2f696d616765732f706f7374732f74657374696e672e676966) 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. 57 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 = %w[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] 29 | -------------------------------------------------------------------------------- /bin/rubycritic: -------------------------------------------------------------------------------- 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 11 | -------------------------------------------------------------------------------- /docs/core-metrics.md: -------------------------------------------------------------------------------- 1 | # Core Metrics, Score and Rating 2 | 3 | RubyCritic wraps around static analysis gems such as [Reek][2], [Flay][3] and [Flog][4] to provide a quality report of your Ruby code. 4 | 5 | Each of these gems are internally wrapped as an **Analyser**, with a collection of **AnalysedModule**s, which give us calculations of key metrics. 6 | The most important ones are **churn**, **complexity**, **cost** and **rating**. 7 | 8 | The output of RubyCritic will give you four values to help you judge your code's _odorousness_: 9 | 10 | - [**Score**](#score): A generic number representing overall quality of the analysed code 11 | - [**Churn**](#churn-and-complexity): Number of times a file was changed 12 | - [**Complexity**](#churn-and-complexity): Amount of _pain_ in your code 13 | - [**Rating**](#rating): The grade assigned to a file 14 | 15 | RubyCritic's **rating** system was inspired by Code Climate's, you can [read more about how that works here][1]. 16 | Note that the global **score** is fairly different from Code Climate's GPA, although it serves the same purpose. 17 | 18 | ## Score 19 | 20 | Is a value that ranges from 0 to 100, where higher values are better (less code smells detected) and is intended to provide a quick insight about the overall code quality. 21 | 22 | This is basically an average of the calculated [cost](#cost) of each file. 23 | There is a [threshold][6] to avoid very bad modules from having excessive impact. 24 | 25 | ## Churn and complexity 26 | 27 | Churn is very simple to calculate - it is the number of times the file was committed. 28 | 29 | Complexity is the output of [Flog][4]. You can [read more about how it works][7], but here's a quick summary: 30 | 31 | > It works based on counting your code's ABCs: 32 | > 33 | > A - Assignments. When more objects are assigned, the complexity goes up - foo = 1 + 1. 34 | > 35 | > B - Branches. When code branches, there are multiple paths that it might follow. This also increases it's complexity. 36 | > 37 | > C - Calls. When code calls other code, the complexity increases because they caller and callee are now connected. A call is both a method call or other action like eval or puts. 38 | > 39 | > All code has assignments, branches, and calls. Flog's job is to check that they aren't used excessively or abused. 40 | 41 | Both **churn** and **complexity** are presented as a chart: 42 | 43 | ![RubyCritic overview screenshot](/images/churn-vs-complexity.png) 44 | 45 | Each file is represented by a dot. **The closer they are to the bottom-left corner, the better.** 46 | But keep in mind that you cannot reduce churn (well... not unless you re-write your repo's history :neckbeard:), so try to keep the dots as close to the bottom as possible. 47 | Chad made a nice [summary if you want to know more][8] about the meaning behind each quadrant. 48 | 49 | ## Rating 50 | 51 | This is a letter from `A` to `F`, `A` being the best. This serves as a baseline to tell you how *smelly* a file is. 52 | Generally `A`'s & `B`'s are good enough, `C`'s serve as a warning and `D`'s & `F`'s are the ones you should be fixing. 53 | 54 | **Rating** is simply [a conversion][5] from the calculated **cost** to a letter. 55 | 56 | ![RubyCritic code index screenshot](/images/rating.png) 57 | 58 | ### Cost 59 | 60 | The definition of this **cost** varies from tool to tool, but it's always a non-negative number, with high values indicating worse smells. 61 | The **complexity** of a file also (slightly) affects its final cost. 62 | 63 | [1]: https://gist.github.com/brynary/21369b5892525e1bd102 64 | [2]: https://github.com/troessner/reek 65 | [3]: https://github.com/seattlerb/flay 66 | [4]: https://github.com/seattlerb/flog 67 | [5]: https://github.com/whitesmith/rubycritic/blob/main/lib/rubycritic/core/rating.rb 68 | [6]: https://github.com/whitesmith/rubycritic/blob/main/lib/rubycritic/core/analysed_modules_collection.rb 69 | [7]: http://www.railsinside.com/tutorials/487-how-to-score-your-rails-apps-complexity-before-refactoring.html 70 | [8]: https://github.com/chad/turbulence#hopefully-meaningful-metrics 71 | -------------------------------------------------------------------------------- /docs/formatters.md: -------------------------------------------------------------------------------- 1 | # Concept of formatters 2 | 3 | The formatters goal is to allow to extract the logic around the representation of each rubycritic run from the gathering of results. 4 | By delegating to a formatter you can write your own *custom* report for rubycritic and being independent on the logic. 5 | 6 | ## Formatters interface 7 | 8 | The formatters are nothing more than a class similar to those on [report/generator](/lib/rubycritic/generators). 9 | The report generator must accept an array of `analysed_modules` when initialized, and respond to `#generate_report`, for example: 10 | 11 | ``` ruby 12 | class MyFormatter 13 | def initialize(analysed_modules) 14 | .. 15 | @analysed_modules = analysed_modules 16 | .. 17 | end 18 | 19 | def generate_report 20 | .. # do whatever you want with the analysed_modules model 21 | end 22 | end 23 | ``` 24 | 25 | The results will be passed through the [analysed_modules_collection class](/lib/rubycritic/core/analysed_modules_collection.rb). 26 | The `generate_report` method is called to actually generate the report. 27 | 28 | ## Examples 29 | 30 | ### Badges 31 | 32 | See [rubycritic-small-badge](https://github.com/MarcGrimme/rubycritic-small-badge/). This badge could look as follows: 33 | 34 | [![RubyCritic](https://marcgrimme.github.io/rubycritic-small-badge/badges/rubycritic_badge_score.svg)](https://marcgrimme.github.io/rubycritic-small-badge/tmp/rubycritic/overview.html) 35 | 36 | ## Formatter Classloading 37 | 38 | ### With classname 39 | 40 | In order to load the formatter class as part of the Raketask the following approach can be followed. 41 | Basically the *require* of the formatter class needs to happen somewhere before the `RubyCritic::RakeTask` is defined. 42 | 43 | When *rubycritic* is run, the formatter class is automatically loaded if the formatter parameter represents the fully qualified classname of the formatter class. 44 | Taking the above example and combining it with the *Rakefile* could look as follows: 45 | 46 | ``` ruby Rakefile 47 | ... 48 | require 'my_formatter' 49 | RubyCritic::RakeTask.new do |task| 50 | .. 51 | task.options = %(--custom-format MyFormatter) 52 | .. 53 | end 54 | ``` 55 | 56 | See the [Rakefile](https://github.com/MarcGrimme/repo-small-badge/blob/master/Rakefile#L14-L19) for [rubycritic-small-badge](https://github.com/MarcGrimme/rubycritic-small-badge/) 57 | 58 | ### With classname and classpath 59 | 60 | When *rubycritic* is called outside of the structure that has to be **criticized** with just calling the command. 61 | The path to load as well, as the fully qualified classname of the formatter, have to be passed. 62 | This happens with the `--custom-format` option followed by the path to require (*requirepath*) a `:` as a separator and the fully qualified *classname*. 63 | An example could look as follows: 64 | 65 | ``` shell 66 | gem install my_formatter_gem 67 | rubycritic --custom-format my_formatter:MyFormatter 68 | ``` 69 | 70 | This will do the same as above. 71 | -------------------------------------------------------------------------------- /docs/jenkins-pr-reviews.md: -------------------------------------------------------------------------------- 1 | # Making Jenkins review Pull Requests 2 | 3 | ## Installing Jenkins and setting up RubyCritic 4 | 5 | There is a step-by-step tutorial on how to set up Jenkins in [`building-own-code-climate.md`](./building-own-code-climate.md). 6 | 7 | ### Installing required plugins 8 | 9 | Installing a Jenkins Plugin is extremely easy. All you have to do is proceed to `Manage Jenkins`, go to `Manage Plugins`, select the `Available` tab and then check the desired plugin. 10 | 11 | For creating comments on PRs we are going to use the [Violation Comments to GitHub Jenkins Plugin](https://github.com/jenkinsci/violation-comments-to-github-plugin). 12 | 13 | ## Configuring the project 14 | 15 | The Violation plugin has parsers for many different formats, and the GoLint one is compatible with the `lint` format created by RubyCritic. 16 | We're assuming that you use a `Jenkinsfile` for creating a pipeline, but the approach can be adapted to other scenarios. 17 | 18 | ```groovy 19 | pipeline { 20 | agent any 21 | 22 | stages { 23 | stage('Build') { 24 | steps { 25 | // Install gems etc. 26 | } 27 | } 28 | stage('Test') { 29 | steps { 30 | parallel tests: { // We are running tests and code_checks in parallel to shorten build times 31 | sh 'bundle exec rspec' 32 | }, 33 | code_checks: { 34 | sh 'bundle exec rubycritic -f lint' 35 | } 36 | } 37 | } 38 | stage('Package / Deploy') { 39 | steps { 40 | parallel deploy: { 41 | // Create Docker image / deploy via Capistrano / ... 42 | }, 43 | publish_code_review: { 44 | step([ 45 | $class: 'ViolationsToGitHubRecorder', 46 | config: [ 47 | repositoryName: 'your_project_name', 48 | pullRequestId: env.CHANGE_ID, // The CHANGE_ID env variable will be set to the PR ID by Jenkins 49 | createSingleFileComments: true, // Create one comment per violation 50 | commentOnlyChangedContent: true, // Only comment on lines that have changed 51 | keepOldComments: false, 52 | violationConfigs: [ 53 | [ pattern: '.*/lint\\.txt$', parser: 'GOLINT', reporter: 'RubyCritic' ], // RubyCritic will output a lint.txt file in GoLint compatible format 54 | ] 55 | ]]) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | For further information, check out the documentation of the [Violation Comments to GitHub Jenkins Plugin](https://github.com/jenkinsci/violation-comments-to-github-plugin). 64 | -------------------------------------------------------------------------------- /features/command_line_interface/minimum_score.feature: -------------------------------------------------------------------------------- 1 | Feature: Break if overall score is below minimum 2 | In order to break the Continuous Integration builds based on a score threshold 3 | RubyCritic returns the exit status according with the score 4 | 5 | Scenario: Status indicates a success when not using --minimum-score 6 | Given the smelly file 'smelly.rb' with a score of 93.19 7 | When I run rubycritic smelly.rb 8 | Then the exit status indicates a success 9 | 10 | Scenario: Status indicates an error when score below the minimum 11 | Given the smelly file 'smelly.rb' with a score of 93.19 12 | When I run rubycritic --minimum-score 100 smelly.rb 13 | Then the exit status indicates an error 14 | 15 | Scenario: Status indicates a success when score is above the minimum 16 | Given the smelly file 'smelly.rb' with a score of 93.19 17 | When I run rubycritic --minimum-score 93 smelly.rb 18 | Then the exit status indicates a success 19 | 20 | Scenario: Status indicates a success when score is equal the minimum 21 | Given the clean file 'clean.rb' with a score of 100 22 | When I run rubycritic --minimum-score 100 clean.rb 23 | Then the exit status indicates a success 24 | 25 | Scenario: Prints the score on output 26 | Given the smelly file 'smelly.rb' with a score of 93.19 27 | When I run rubycritic smelly.rb 28 | Then the output should contain: 29 | """ 30 | Score: 93.19 31 | """ 32 | 33 | Scenario: Prints a message informing the score is below the minimum 34 | Given the empty file 'empty.rb' with a score of 0 35 | When I run rubycritic --minimum-score 100 empty.rb 36 | Then the output should contain: 37 | """ 38 | Score (0.0) is below the minimum 100 39 | """ 40 | -------------------------------------------------------------------------------- /features/command_line_interface/options.feature: -------------------------------------------------------------------------------- 1 | Feature: RubyCritic can be controlled using command-line options 2 | In order to change RubyCritic's default behaviour 3 | As a developer 4 | I want to supply options on the command line 5 | 6 | Scenario: return non-zero status on bad option 7 | When I run rubycritic --no-such-option 8 | Then the exit status indicates an error 9 | And it reports the error "Error: invalid option: --no-such-option" 10 | And there is no output on stdout 11 | 12 | Scenario: display the current version number 13 | When I run rubycritic --version 14 | Then it succeeds 15 | And it reports the current version 16 | 17 | Scenario: display the help information 18 | When I run rubycritic --help 19 | Then it succeeds 20 | And it reports: 21 | """ 22 | Usage: rubycritic [options] [paths] 23 | -p, --path [PATH] Set path where report will be saved (tmp/rubycritic by default) 24 | -b, --branch BRANCH Set branch to compare 25 | -t [MAX_DECREASE], Set a threshold for score difference between two branches (works only with -b) 26 | --maximum-decrease 27 | -f, --format [FORMAT] Report smells in the given format: 28 | html (default; will open in a browser) 29 | json 30 | console 31 | lint 32 | Multiple formats are supported. 33 | --custom-format [REQUIREPATH]:[CLASSNAME]|[CLASSNAME] 34 | Instantiate a given class as formatter and call report for reporting. 35 | Two ways are possible to load the formatter. 36 | If the class is not autorequired the REQUIREPATH can be given together 37 | with the CLASSNAME to be loaded separated by a :. 38 | Example: rubycritic/markdown/reporter.rb:RubyCritic::MarkDown::Reporter 39 | or if the file is already required the CLASSNAME is enough 40 | Example: RubyCritic::MarkDown::Reporter 41 | Multiple formatters are supported. 42 | -s, --minimum-score [MIN_SCORE] Set a minimum score 43 | --churn-after [DATE] Only count churn from a certain date. 44 | The date is passed through to version control (currently git only). 45 | Example: 2017-01-01 46 | -m, --mode-ci [BASE_BRANCH] Use CI mode (faster, analyses diffs w.r.t base_branch (default: main)) 47 | --deduplicate-symlinks De-duplicate symlinks based on their final target 48 | --suppress-ratings Suppress letter ratings 49 | --no-browser Do not open html report with browser 50 | --coverage-path [PATH] SimpleCov coverage will be saved (./coverage by default) 51 | -v, --version Show gem's version 52 | -h, --help Show this message 53 | 54 | """ 55 | -------------------------------------------------------------------------------- /features/rake_task.feature: -------------------------------------------------------------------------------- 1 | Feature: RubyCritic can be run via Rake task 2 | In order to allow for a better CI usage 3 | As a developer 4 | I want to use RubyCritic as a Rake task 5 | 6 | Scenario: ‘paths' attribute is respected 7 | Given the smelly file 'smelly.rb' 8 | When I run rake rubycritic with: 9 | """ 10 | RubyCritic::RakeTask.new do |t| 11 | t.paths = FileList['smelly.*'] 12 | t.options = '--no-browser -f console' 13 | end 14 | """ 15 | Then the output should contain: 16 | """ 17 | (HighComplexity) AllTheMethods#method_missing has a flog score of 27 18 | """ 19 | And the exit status indicates a success 20 | 21 | Scenario: 'name' option changes the task name 22 | Given the smelly file 'smelly.rb' 23 | When I run rake silky with: 24 | """ 25 | RubyCritic::RakeTask.new('silky') do |t| 26 | t.paths = FileList['smelly.*'] 27 | t.verbose = true 28 | t.options = '--no-browser' 29 | end 30 | """ 31 | Then the output should contain: 32 | """ 33 | Running `silky` rake command 34 | """ 35 | 36 | Scenario: 'verbose' prints details about the execution 37 | Given the smelly file 'smelly.rb' 38 | When I run rake rubycritic with: 39 | """ 40 | RubyCritic::RakeTask.new do |t| 41 | t.paths = FileList['smelly.*'] 42 | t.verbose = true 43 | t.options = '--no-browser' 44 | end 45 | """ 46 | Then the output should contain: 47 | """ 48 | !!! Running `rubycritic` rake command 49 | !!! Inspecting smelly.rb with options --no-browser 50 | """ 51 | 52 | Scenario: respect --minimum-score 53 | Given the smelly file 'smelly.rb' 54 | When I run rake rubycritic with: 55 | """ 56 | RubyCritic::RakeTask.new do |t| 57 | t.paths = FileList['smelly.*'] 58 | t.verbose = true 59 | t.options = '--no-browser -f console --minimum-score 95' 60 | end 61 | """ 62 | Then the output should contain: 63 | """ 64 | Score (93.19) is below the minimum 95 65 | """ 66 | And the exit status indicates an error 67 | 68 | Scenario: 'fail_on_error' when false will exit 0 even when RubyCritic fails 69 | Given the smelly file 'smelly.rb' 70 | When I run rake rubycritic with: 71 | """ 72 | RubyCritic::RakeTask.new do |t| 73 | t.paths = FileList['smelly.*'] 74 | t.fail_on_error = false 75 | t.options = '--no-browser -f console --minimum-score 95' 76 | end 77 | """ 78 | Then the output should contain: 79 | """ 80 | Score (93.19) is below the minimum 95 81 | """ 82 | And the exit status indicates a success 83 | -------------------------------------------------------------------------------- /features/step_definitions/rake_task_steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | When(/^I run rake (\w*) with:$/) do |name, task_def| 4 | rake(name, task_def) 5 | end 6 | -------------------------------------------------------------------------------- /features/step_definitions/rubycritic_steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | When(/^I run rubycritic (.*)$/) do |args| 4 | rubycritic(args) 5 | end 6 | 7 | Then(/^the exit status indicates an error$/) do 8 | expect(last_command_started).to have_exit_status(RubyCritic::Command::StatusReporter::SCORE_BELOW_MINIMUM) 9 | end 10 | 11 | Then(/^the exit status indicates a success$/) do 12 | expect(last_command_started).to have_exit_status(RubyCritic::Command::StatusReporter::SUCCESS) 13 | end 14 | 15 | Then(/^it reports:$/) do |report| 16 | expect(last_command_started).to have_output_on_stdout(report.gsub('\n', "\n")) 17 | end 18 | 19 | Then(/^there is no output on stdout$/) do 20 | expect(last_command_started).to have_output_on_stdout('') 21 | end 22 | 23 | Then(/^it reports the current version$/) do 24 | expect(last_command_started).to have_output("RubyCritic #{RubyCritic::VERSION}\n") 25 | end 26 | 27 | Then(/^it reports the error ['"](.*)['"]$/) do |string| 28 | expect(last_command_started).to have_output_on_stderr(/#{Regexp.escape(string)}/) 29 | end 30 | 31 | Then(/^it succeeds$/) do 32 | expect(last_command_started).to have_exit_status(RubyCritic::Command::StatusReporter::SUCCESS) 33 | end 34 | -------------------------------------------------------------------------------- /features/step_definitions/sample_file_steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Given(/^the smelly file 'smelly.rb'/) do 4 | contents = <<~RUBY 5 | class AllTheMethods 6 | def method_missing(method, *args, &block) 7 | message = "I" 8 | eval "message = ' did not'" 9 | eval "message << ' exist,'" 10 | eval "message << ' but now'" 11 | eval "message << ' I do.'" 12 | self.class.send(:define_method, method) { "I did not exist, but now I do." } 13 | self.send(method) 14 | end 15 | end 16 | RUBY 17 | write_file('smelly.rb', contents) 18 | end 19 | 20 | Given(/^the clean file 'clean.rb'/) do 21 | contents = <<~RUBY 22 | # Explanatory comment 23 | class Clean 24 | def foo; end 25 | end 26 | RUBY 27 | write_file('clean.rb', contents) 28 | end 29 | 30 | Given(/^the empty file 'empty.rb'/) do 31 | write_file('clean.rb', '') 32 | end 33 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../lib/rubycritic' 4 | require_relative '../../lib/rubycritic/cli/application' 5 | require_relative '../../lib/rubycritic/commands/status_reporter' 6 | require 'aruba/cucumber' 7 | require 'minitest/spec' 8 | 9 | # 10 | # Provides runner methods used in the cucumber steps. 11 | # 12 | class RubyCriticWorld 13 | extend Minitest::Assertions 14 | attr_accessor :assertions 15 | 16 | def initialize 17 | self.assertions = 0 18 | end 19 | 20 | def rubycritic(args) 21 | run_command_and_stop( 22 | "rubycritic #{args} --no-browser", 23 | fail_on_error: false 24 | ) 25 | end 26 | 27 | def rake(name, task_def) 28 | header = <<~RUBY 29 | require 'rubycritic' 30 | require 'rubycritic/rake_task' 31 | 32 | RUBY 33 | write_file 'Rakefile', header + task_def 34 | run_command_and_stop( 35 | "rake #{name}", 36 | fail_on_error: false 37 | ) 38 | end 39 | end 40 | 41 | World do 42 | RubyCriticWorld.new 43 | end 44 | 45 | Before do 46 | Aruba.configure do |config| 47 | config.exit_timeout = 30 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /gemfiles/simplecov_0.17.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source 'https://rubygems.org' 6 | 7 | gem 'simplecov', '0.17' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /gemfiles/simplecov_0.18.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source 'https://rubygems.org' 6 | 7 | gem 'simplecov', '0.18' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /gemfiles/simplecov_0.19.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source 'https://rubygems.org' 6 | 7 | gem 'simplecov', '0.19' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /images/churn-vs-complexity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/churn-vs-complexity.png -------------------------------------------------------------------------------- /images/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/code.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/logo.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/overview.png -------------------------------------------------------------------------------- /images/rating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/rating.png -------------------------------------------------------------------------------- /images/reek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/reek.png -------------------------------------------------------------------------------- /images/smell-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/smell-details.png -------------------------------------------------------------------------------- /images/smells.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/smells.png -------------------------------------------------------------------------------- /images/whitesmith.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/images/whitesmith.png -------------------------------------------------------------------------------- /lib/rubycritic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/command_factory' 4 | 5 | module RubyCritic 6 | end 7 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/attributes.rb: -------------------------------------------------------------------------------- 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 30 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/churn.rb: -------------------------------------------------------------------------------- 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 31 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/complexity.rb: -------------------------------------------------------------------------------- 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 31 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/coverage.rb: -------------------------------------------------------------------------------- 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 | RESULTSET_FILENAME = '.resultset.json' 13 | 14 | def initialize(analysed_modules) 15 | @analysed_modules = analysed_modules 16 | @result = results.first 17 | end 18 | 19 | def run 20 | @analysed_modules.each do |analysed_module| 21 | analysed_module.coverage = find_coverage_percentage(analysed_module) 22 | print green '.' 23 | end 24 | puts '' 25 | end 26 | 27 | def to_s 28 | 'simple_cov' 29 | end 30 | 31 | private 32 | 33 | def find_coverage_percentage(analysed_module) 34 | source_file = find_source_file(analysed_module) 35 | 36 | return 0 unless source_file 37 | 38 | source_file.covered_percent 39 | end 40 | 41 | def find_source_file(analysed_module) 42 | return unless @result 43 | 44 | needle = File.join(SimpleCov.root, analysed_module.path) 45 | 46 | @result.source_files.detect { |file| file.filename == needle } 47 | end 48 | 49 | # The path to the cache file 50 | def resultset_path 51 | if (cp = Config.coverage_path) 52 | SimpleCov.coverage_dir(cp) 53 | end 54 | File.join(SimpleCov.coverage_path, RESULTSET_FILENAME) 55 | end 56 | 57 | def resultset_writelock 58 | "#{resultset_path}.lock" 59 | end 60 | 61 | # Loads the cached resultset from JSON and returns it as a Hash, 62 | # caching it for subsequent accesses. 63 | def resultset 64 | @resultset ||= parse_resultset(stored_data) 65 | end 66 | 67 | def parse_resultset(data) 68 | return {} unless data 69 | 70 | JSON.parse(data) || {} 71 | rescue JSON::ParserError => err 72 | puts "Error: Loading #{RESULTSET_FILENAME}: #{err.message}" 73 | {} 74 | end 75 | 76 | # Returns the contents of the resultset cache as a string or if the file is missing or empty nil 77 | def stored_data 78 | synchronize_resultset do 79 | return unless File.exist?(resultset_path) 80 | 81 | return unless (data = File.read(resultset_path)) 82 | 83 | return if data.length < 2 84 | 85 | data 86 | end 87 | end 88 | 89 | # Ensure only one process is reading or writing the resultset at any 90 | # given time 91 | def synchronize_resultset(&) 92 | # make it reentrant 93 | return yield if defined?(@resultset_locked) && @resultset_locked == true 94 | 95 | return yield unless File.exist?(resultset_writelock) 96 | 97 | with_lock(&) 98 | end 99 | 100 | def with_lock 101 | @resultset_locked = true 102 | File.open(resultset_writelock, 'w+') do |file| 103 | file.flock(File::LOCK_EX) 104 | yield 105 | end 106 | ensure 107 | @resultset_locked = false 108 | end 109 | 110 | # Gets the resultset hash and re-creates all included instances 111 | # of SimpleCov::Result from that. 112 | # All results that are above the SimpleCov.merge_timeout will be 113 | # dropped. Returns an array of SimpleCov::Result items. 114 | def results 115 | if Gem.loaded_specs['simplecov'].version >= Gem::Version.new('0.19') 116 | ::SimpleCov::Result.from_hash(resultset) 117 | else 118 | resultset.map { |command_name, data| ::SimpleCov::Result.from_hash(command_name => data) } 119 | end 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/helpers/ast_node.rb: -------------------------------------------------------------------------------- 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(&block) 17 | children.each do |child| 18 | next unless child.is_a?(Parser::AST::Node) 19 | 20 | yield child 21 | child.recursive_children(&block) 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 77 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/helpers/flay.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'flay' 4 | 5 | module RubyCritic 6 | class Flay < ::Flay 7 | def initialize(paths) 8 | super() 9 | paths = PathExpander.new([], '').filter_files(paths, DEFAULT_IGNORE) 10 | process(*paths) 11 | analyze 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/helpers/flog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'flog' 4 | 5 | module RubyCritic 6 | class Flog < ::Flog 7 | DEFAULT_OPTIONS = { 8 | all: true, 9 | continue: true, 10 | methods: true 11 | }.freeze 12 | 13 | def initialize 14 | super(DEFAULT_OPTIONS) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/helpers/methods_counter.rb: -------------------------------------------------------------------------------- 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 26 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/helpers/modules_locator.rb: -------------------------------------------------------------------------------- 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 | name_from_path.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 43 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/helpers/parser.rb: -------------------------------------------------------------------------------- 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 15 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/helpers/reek.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'reek' 4 | 5 | module RubyCritic 6 | class Reek < ::Reek::Examiner 7 | def self.configuration 8 | @configuration ||= ::Reek::Configuration::AppConfiguration.from_default_path 9 | end 10 | 11 | def initialize(analysed_module) 12 | super(analysed_module, configuration: self.class.configuration) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/smells/flay.rb: -------------------------------------------------------------------------------- 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 80 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/smells/flog.rb: -------------------------------------------------------------------------------- 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 70 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers/smells/reek.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/analysers/helpers/reek' 4 | require 'rubycritic/core/smell' 5 | require 'rubycritic/colorize' 6 | 7 | module RubyCritic 8 | module Analyser 9 | class ReekSmells 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 | add_smells_to(analysed_module) 18 | print green '.' 19 | end 20 | puts '' 21 | end 22 | 23 | def to_s 24 | 'reek smells' 25 | end 26 | 27 | private 28 | 29 | def add_smells_to(analysed_module) 30 | Reek.new(analysed_module.pathname).smells.each do |smell| 31 | analysed_module.smells << create_smell(smell) 32 | end 33 | end 34 | 35 | def create_smell(smell) 36 | Smell.new( 37 | locations: smell_locations(smell.source, smell.lines), 38 | context: smell.context, 39 | message: smell.message, 40 | type: smell.smell_type, 41 | analyser: 'reek', 42 | cost: 0 43 | ) 44 | end 45 | 46 | def smell_locations(file_path, file_lines) 47 | file_lines.uniq.map do |file_line| 48 | Location.new(file_path, file_line) 49 | end.sort 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/rubycritic/analysers_runner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/core/analysed_modules_collection' 4 | require 'rubycritic/analysers/smells/flay' 5 | require 'rubycritic/analysers/smells/flog' 6 | require 'rubycritic/analysers/smells/reek' 7 | require 'rubycritic/analysers/complexity' 8 | require 'rubycritic/analysers/churn' 9 | require 'rubycritic/analysers/attributes' 10 | require 'rubycritic/analysers/coverage' 11 | 12 | module RubyCritic 13 | class AnalysersRunner 14 | ANALYSERS = [ 15 | Analyser::FlaySmells, 16 | Analyser::FlogSmells, 17 | Analyser::ReekSmells, 18 | Analyser::Complexity, 19 | Analyser::Attributes, 20 | Analyser::Churn, 21 | Analyser::Coverage 22 | ].freeze 23 | 24 | def initialize(paths) 25 | @paths = paths 26 | end 27 | 28 | def run 29 | ANALYSERS.each do |analyser_class| 30 | analyser_instance = analyser_class.new(analysed_modules) 31 | puts "running #{analyser_instance}" 32 | analyser_instance.run 33 | end 34 | analysed_modules 35 | end 36 | 37 | def analysed_modules 38 | @analysed_modules ||= AnalysedModulesCollection.new(@paths) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/rubycritic/analysis_summary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | class AnalysisSummary 5 | def self.generate(analysed_modules) 6 | new(analysed_modules).generate 7 | end 8 | 9 | def initialize(analysed_modules) 10 | @analysed_modules = analysed_modules 11 | end 12 | 13 | def generate 14 | %w[A B C D F].each_with_object({}) do |rating, summary| 15 | summary[rating] = generate_for(rating) 16 | end 17 | end 18 | 19 | private 20 | 21 | attr_reader :analysed_modules, :modules 22 | 23 | def generate_for(rating) 24 | @modules = analysed_modules.for_rating(rating) 25 | { 26 | files: modules.count, 27 | churns: churns, 28 | smells: smells 29 | } 30 | end 31 | 32 | def churns 33 | modules.inject(0) { |acc, elem| acc + elem.churn } 34 | end 35 | 36 | def smells 37 | modules.inject(0) { |acc, elem| acc + elem.smells.count } 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/rubycritic/browser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'launchy' 4 | 5 | module RubyCritic 6 | class Browser 7 | attr_reader :report_path 8 | 9 | def initialize(report_path) 10 | @report_path = report_path 11 | end 12 | 13 | def open 14 | Launchy.open(report_path) do |exception| 15 | puts "Attempted to open #{report_path} and failed because #{exception}" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rubycritic/cli/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic' 4 | require 'rubycritic/browser' 5 | require 'rubycritic/cli/options' 6 | require 'rubycritic/command_factory' 7 | 8 | module RubyCritic 9 | module Cli 10 | class Application 11 | STATUS_SUCCESS = 0 12 | STATUS_ERROR = 1 13 | 14 | def initialize(argv) 15 | @options = Options.new(argv) 16 | end 17 | 18 | def execute 19 | parsed_options = @options.parse.to_h 20 | 21 | reporter = RubyCritic::CommandFactory.create(parsed_options).execute 22 | print(reporter.status_message) 23 | reporter.status 24 | rescue OptionParser::InvalidOption => error 25 | warn "Error: #{error}" 26 | STATUS_ERROR 27 | end 28 | 29 | def print(message) 30 | $stdout.puts message 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/rubycritic/cli/options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/cli/options/argv' 4 | require 'rubycritic/cli/options/file' 5 | 6 | module RubyCritic 7 | module Cli 8 | class Options 9 | attr_reader :argv_options, :file_options 10 | 11 | def initialize(argv) 12 | @argv_options = Argv.new(argv) 13 | @file_options = File.new 14 | end 15 | 16 | def parse 17 | argv_options.parse 18 | file_options.parse 19 | self 20 | end 21 | 22 | # :reek:NilCheck 23 | def to_h 24 | file_hash = file_options.to_h 25 | argv_hash = argv_options.to_h 26 | 27 | file_hash.merge(argv_hash) do |_, file_option, argv_option| 28 | Array(argv_option).empty? ? file_option : argv_option 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rubycritic/cli/options/file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module RubyCritic 6 | module Cli 7 | class Options 8 | class File 9 | attr_reader :filename, :options 10 | 11 | def initialize(filename = './.rubycritic.yml') 12 | @filename = filename 13 | @options = {} 14 | end 15 | 16 | def parse 17 | @options = YAML.load_file(filename) if ::File.file?(filename) 18 | end 19 | 20 | # rubocop:disable Metrics/MethodLength 21 | def to_h 22 | { 23 | mode: mode, 24 | root: root, 25 | coverage_path: coverage_path, 26 | formats: formats, 27 | deduplicate_symlinks: deduplicate_symlinks, 28 | paths: paths, 29 | suppress_ratings: suppress_ratings, 30 | minimum_score: minimum_score, 31 | no_browser: no_browser, 32 | base_branch: base_branch, 33 | feature_branch: feature_branch, 34 | threshold_score: threshold_score 35 | } 36 | end 37 | # rubocop:enable Metrics/MethodLength 38 | 39 | private 40 | 41 | def base_branch 42 | return options.dig('mode_ci', 'branch') || 'main' if options.dig('mode_ci', 'enabled').to_s == 'true' 43 | 44 | options['branch'] 45 | end 46 | 47 | def mode 48 | if options.dig('mode_ci', 'enabled').to_s == 'true' 49 | :ci 50 | elsif base_branch 51 | :compare_branches 52 | end 53 | end 54 | 55 | def feature_branch 56 | SourceControlSystem::Git.current_branch if base_branch 57 | end 58 | 59 | def root 60 | options['path'] 61 | end 62 | 63 | def coverage_path 64 | options['coverage_path'] 65 | end 66 | 67 | def threshold_score 68 | options['threshold_score'] 69 | end 70 | 71 | def deduplicate_symlinks 72 | value_for(options['deduplicate_symlinks']) 73 | end 74 | 75 | def suppress_ratings 76 | value_for(options['suppress_ratings']) 77 | end 78 | 79 | def no_browser 80 | value_for(options['no_browser']) 81 | end 82 | 83 | def formats 84 | formats = Array(options['formats']) 85 | formats_to_check = %w[html json console lint] 86 | formats.select do |format| 87 | formats_to_check.include?(format) 88 | end.map(&:to_sym) 89 | end 90 | 91 | def minimum_score 92 | options['minimum_score'] 93 | end 94 | 95 | def paths 96 | options['paths'] 97 | end 98 | 99 | def value_for(value) 100 | value = value.to_s 101 | value == 'true' unless value.empty? 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/rubycritic/colorize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module Colorize 5 | def colorize(text, color_code) 6 | "\e[#{color_code}m#{text}\e[0m" 7 | end 8 | 9 | def red(text) 10 | colorize(text, 31) 11 | end 12 | 13 | def green(text) 14 | colorize(text, 32) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rubycritic/command_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/configuration' 4 | 5 | module RubyCritic 6 | class CommandFactory 7 | COMMAND_CLASS_MODES = %i[version help ci compare default].freeze 8 | 9 | def self.create(options = {}) 10 | Config.set(options) 11 | command_class(Config.mode).new(options) 12 | end 13 | 14 | def self.command_class(mode) 15 | mode = mode.to_s.split('_').first.to_sym 16 | if COMMAND_CLASS_MODES.include? mode 17 | require "rubycritic/commands/#{mode}" 18 | Command.const_get(mode.capitalize) 19 | else 20 | require 'rubycritic/commands/default' 21 | Command::Default 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rubycritic/commands/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/commands/status_reporter' 4 | 5 | module RubyCritic 6 | module Command 7 | class Base 8 | def initialize(options) 9 | @options = options 10 | @status_reporter = RubyCritic::Command::StatusReporter.new(@options) 11 | end 12 | 13 | def execute 14 | raise NotImplementedError 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rubycritic/commands/ci.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/source_control_systems/base' 4 | require 'rubycritic/analysers_runner' 5 | require 'rubycritic/reporter' 6 | require 'rubycritic/commands/default' 7 | require 'rubycritic/commands/compare' 8 | 9 | module RubyCritic 10 | module Command 11 | class Ci < Compare 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/rubycritic/commands/default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/source_control_systems/base' 4 | require 'rubycritic/analysers_runner' 5 | require 'rubycritic/revision_comparator' 6 | require 'rubycritic/reporter' 7 | require 'rubycritic/commands/base' 8 | 9 | module RubyCritic 10 | module Command 11 | class Default < Base 12 | def initialize(options) 13 | super 14 | @paths = options[:paths] || ['.'] 15 | Config.source_control_system = SourceControlSystem::Base.create 16 | end 17 | 18 | def execute 19 | report(critique) 20 | status_reporter 21 | end 22 | 23 | def critique 24 | analysed_modules = AnalysersRunner.new(paths).run 25 | RevisionComparator.new(paths).statuses = analysed_modules 26 | end 27 | 28 | def report(analysed_modules) 29 | Reporter.generate_report(analysed_modules) 30 | status_reporter.score = analysed_modules.score 31 | end 32 | 33 | private 34 | 35 | attr_reader :paths, :status_reporter 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/rubycritic/commands/help.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/commands/base' 4 | 5 | module RubyCritic 6 | module Command 7 | class Help < Base 8 | def execute 9 | puts options[:help_text] 10 | status_reporter 11 | end 12 | 13 | private 14 | 15 | attr_reader :options, :status_reporter 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rubycritic/commands/status_reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module Command 5 | class StatusReporter 6 | attr_reader :status, :status_message, :score 7 | 8 | SUCCESS = 0 9 | SCORE_BELOW_MINIMUM = 1 10 | 11 | def initialize(options) 12 | @options = options 13 | @status = SUCCESS 14 | end 15 | 16 | def score=(input_score) 17 | @score = input_score.round(2) 18 | update_status 19 | end 20 | 21 | private 22 | 23 | def update_status 24 | @status = current_status 25 | update_status_message 26 | end 27 | 28 | def current_status 29 | satisfy_minimum_score_rule ? SUCCESS : SCORE_BELOW_MINIMUM 30 | end 31 | 32 | def satisfy_minimum_score_rule 33 | score >= @options[:minimum_score].to_f 34 | end 35 | 36 | def update_status_message 37 | case @status 38 | when SUCCESS 39 | @status_message = "Score: #{score}" 40 | when SCORE_BELOW_MINIMUM 41 | @status_message = "Score (#{score}) is below the minimum #{@options[:minimum_score]}" 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/rubycritic/commands/utils/build_number_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module Command 5 | module Utils 6 | class BuildNumberFile 7 | attr_reader :file, :build_number 8 | 9 | def initialize 10 | open_build_number_file 11 | end 12 | 13 | # keep track of the number of builds and 14 | # use this build number to create separate directory for each build 15 | def update_build_number 16 | @build_number = file.read.to_i + 1 17 | write_build_number 18 | build_number 19 | end 20 | 21 | def write_build_number 22 | file.rewind 23 | file.write(build_number) 24 | file.close 25 | end 26 | 27 | def open_build_number_file 28 | root = Config.root 29 | FileUtils.mkdir_p(root) unless File.directory?(root) 30 | location = "#{root}/build_number.txt" 31 | File.new(location, 'a') unless File.exist?(location) 32 | @file = File.open(location, 'r+') 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/rubycritic/commands/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/version' 4 | require 'rubycritic/commands/base' 5 | 6 | module RubyCritic 7 | module Command 8 | class Version < Base 9 | attr_reader :status_reporter 10 | 11 | def execute 12 | puts "RubyCritic #{VERSION}" 13 | status_reporter 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rubycritic/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/source_control_systems/base' 4 | 5 | module RubyCritic 6 | class Configuration 7 | attr_reader :root 8 | attr_accessor :source_control_system, :mode, :formats, :formatters, :deduplicate_symlinks, 9 | :suppress_ratings, :open_with, :no_browser, :base_branch, :coverage_path, 10 | :feature_branch, :base_branch_score, :feature_branch_score, 11 | :base_root_directory, :feature_root_directory, 12 | :compare_root_directory, :threshold_score, :base_branch_collection, 13 | :feature_branch_collection, :churn_after, :ruby_extensions, :paths 14 | 15 | def set(options) 16 | self.mode = options[:mode] || :default 17 | self.root = options[:root] || 'tmp/rubycritic' 18 | self.deduplicate_symlinks = options[:deduplicate_symlinks] 19 | self.suppress_ratings = options[:suppress_ratings] 20 | self.open_with = options[:open_with] 21 | self.no_browser = options[:no_browser] 22 | self.coverage_path = options[:coverage_path] 23 | self.threshold_score = options[:threshold_score].to_i 24 | setup_analysis_targets(options) 25 | setup_version_control(options) 26 | setup_formats(options) 27 | end 28 | 29 | def setup_analysis_targets(options) 30 | self.paths = options[:paths] || ['.'] 31 | self.ruby_extensions = options[:ruby_extensions] || %w[.rb .rake .thor] 32 | end 33 | 34 | def setup_version_control(options) 35 | self.base_branch = options[:base_branch] 36 | self.feature_branch = options[:feature_branch] 37 | self.churn_after = options[:churn_after] 38 | end 39 | 40 | def setup_formats(options) 41 | formats = options[:formats].to_a 42 | self.formats = formats.empty? ? [:html] : formats 43 | self.formatters = options[:formatters] || [] 44 | end 45 | 46 | def root=(path) 47 | @root = File.expand_path(path) 48 | end 49 | 50 | def source_control_present? 51 | source_control_system && 52 | !source_control_system.is_a?(SourceControlSystem::Double) 53 | end 54 | end 55 | 56 | module Config 57 | def self.configuration 58 | @configuration ||= Configuration.new 59 | end 60 | 61 | def self.set(options = {}) 62 | configuration.set(options) 63 | end 64 | 65 | def self.compare_branches_mode? 66 | %i[compare_branches ci].include?(Config.mode) 67 | end 68 | 69 | def self.build_mode? 70 | !Config.no_browser && %i[compare_branches ci].include?(Config.mode) 71 | end 72 | 73 | def self.method_missing(method, ...) 74 | if configuration.respond_to?(method) 75 | configuration.public_send(method, ...) 76 | else 77 | super 78 | end 79 | end 80 | 81 | def self.respond_to_missing?(symbol, include_all = false) 82 | configuration.respond_to?(symbol) || super 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/rubycritic/core/analysed_module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'virtus' 4 | require 'rubycritic/core/rating' 5 | 6 | module RubyCritic 7 | class AnalysedModule 8 | include Virtus.model 9 | 10 | # Complexity is reduced by a factor of 25 when calculating cost 11 | COMPLEXITY_FACTOR = 25.0 12 | 13 | attribute :coverage, Float, default: 0.0 14 | attribute :name 15 | attribute :smells_count 16 | attribute :file_location 17 | attribute :file_name 18 | attribute :line_count 19 | attribute :pathname 20 | attribute :smells, Array, default: [] 21 | attribute :churn 22 | attribute :committed_at 23 | attribute :complexity, Float, default: Float::INFINITY 24 | attribute :duplication, Integer, default: 0 25 | attribute :methods_count 26 | 27 | def path 28 | @path ||= pathname.to_s 29 | end 30 | 31 | def file_location 32 | pathname.dirname 33 | end 34 | 35 | def file_name 36 | pathname.basename 37 | end 38 | 39 | def line_count 40 | File.read(path).each_line.count 41 | end 42 | 43 | def cost 44 | @cost ||= smells.sum(0.0, &:cost) + 45 | (complexity / COMPLEXITY_FACTOR) 46 | end 47 | 48 | def coverage_rating 49 | @coverage_rating ||= Rating.from_cost(100 - coverage) 50 | end 51 | 52 | def rating 53 | @rating ||= Rating.from_cost(cost) 54 | end 55 | 56 | def complexity_per_method 57 | if methods_count.zero? 58 | 'N/A' 59 | else 60 | complexity.fdiv(methods_count).round(1) 61 | end 62 | end 63 | 64 | def smells_count 65 | smells.count 66 | end 67 | 68 | def smells? 69 | !smells.empty? 70 | end 71 | 72 | def smells_at_location(location) 73 | smells.select { |smell| smell.at_location?(location) } 74 | end 75 | 76 | def <=>(other) 77 | [rating.to_s, name] <=> [other.rating.to_s, other.name] 78 | end 79 | 80 | def to_h 81 | { 82 | name: name, path: path, smells: smells, 83 | churn: churn, committed_at: committed_at, complexity: complexity, 84 | duplication: duplication, methods_count: methods_count, cost: cost, 85 | rating: rating 86 | } 87 | end 88 | 89 | def to_json(*options) 90 | to_h.to_json(*options) 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/rubycritic/core/analysed_modules_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/source_locator' 4 | require 'rubycritic/core/analysed_module' 5 | 6 | module RubyCritic 7 | class AnalysedModulesCollection 8 | include Enumerable 9 | 10 | # Limit used to prevent very bad modules to have excessive impact in the 11 | # overall result. See #limited_cost_for 12 | COST_LIMIT = 32.0 13 | # Score goes from 0 (worst) to 100 (perfect) 14 | MAX_SCORE = 100.0 15 | # Projects with an average cost of 16 (or above) will score 0, since 16 16 | # is where the worst possible rating (F) starts 17 | ZERO_SCORE_COST = 16.0 18 | COST_MULTIPLIER = MAX_SCORE / ZERO_SCORE_COST 19 | 20 | def initialize(paths, modules = nil) 21 | @modules = SourceLocator.new(paths).pathnames.map do |pathname| 22 | if modules 23 | analysed_module = modules.find { |mod| mod.pathname == pathname } 24 | build_analysed_module(analysed_module) 25 | else 26 | AnalysedModule.new(pathname: pathname) 27 | end 28 | end 29 | end 30 | 31 | def each(&) 32 | @modules.each(&) 33 | end 34 | 35 | def where(module_paths) 36 | @modules.find_all { |mod| module_paths.include? mod.path } 37 | end 38 | 39 | def find(module_path) 40 | @modules.find { |mod| mod.pathname == module_path } 41 | end 42 | 43 | def to_json(*options) 44 | @modules.to_json(*options) 45 | end 46 | 47 | def score 48 | if @modules.any? 49 | (MAX_SCORE - (average_limited_cost * COST_MULTIPLIER)).round(2) 50 | else 51 | 0.0 52 | end 53 | end 54 | 55 | def summary 56 | AnalysisSummary.generate(self) 57 | end 58 | 59 | def for_rating(rating) 60 | find_all { |mod| mod.rating.to_s == rating } 61 | end 62 | 63 | private 64 | 65 | def average_limited_cost 66 | [average_cost, ZERO_SCORE_COST].min 67 | end 68 | 69 | def average_cost 70 | num_modules = @modules.size 71 | if num_modules.positive? 72 | sum { |mod| limited_cost_for(mod) } / num_modules.to_f 73 | else 74 | 0.0 75 | end 76 | end 77 | 78 | def limited_cost_for(mod) 79 | [mod.cost, COST_LIMIT].min 80 | end 81 | 82 | def build_analysed_module(analysed_module) 83 | AnalysedModule.new( 84 | pathname: analysed_module.pathname, 85 | name: analysed_module.name, 86 | smells: analysed_module.smells, 87 | churn: analysed_module.churn, 88 | committed_at: analysed_module.committed_at, 89 | complexity: analysed_module.complexity, 90 | duplication: analysed_module.duplication, 91 | methods_count: analysed_module.methods_count 92 | ) 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/rubycritic/core/location.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | 5 | module RubyCritic 6 | class Location 7 | attr_reader :pathname, :line 8 | 9 | def initialize(path, line) 10 | @pathname = Pathname.new(path) 11 | @line = line.to_i 12 | end 13 | 14 | def file_name 15 | @pathname.basename.sub_ext('').to_s 16 | end 17 | 18 | def to_s 19 | "#{pathname}:#{line}" 20 | end 21 | 22 | def to_h 23 | { 24 | path: pathname.to_s, 25 | line: line 26 | } 27 | end 28 | 29 | def to_json(*options) 30 | to_h.to_json(*options) 31 | end 32 | 33 | def ==(other) 34 | state == other.state 35 | end 36 | 37 | def <=>(other) 38 | state <=> other.state 39 | end 40 | 41 | protected 42 | 43 | def state 44 | [@pathname, @line] 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/rubycritic/core/rating.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | class Rating 5 | def self.from_cost(cost) 6 | if cost <= 2 then new('A') 7 | elsif cost <= 4 then new('B') 8 | elsif cost <= 8 then new('C') 9 | elsif cost <= 16 then new('D') 10 | else 11 | new('F') 12 | end 13 | end 14 | 15 | def initialize(letter) 16 | @letter = letter 17 | end 18 | 19 | def to_s 20 | @letter 21 | end 22 | 23 | def to_h 24 | @letter 25 | end 26 | 27 | def to_json(*options) 28 | to_h.to_json(*options) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/rubycritic/core/smell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'virtus' 4 | require 'rubycritic/core/location' 5 | 6 | module RubyCritic 7 | class Smell 8 | include Virtus.model 9 | 10 | attribute :context 11 | attribute :cost 12 | attribute :locations, Array, default: [] 13 | attribute :message 14 | attribute :score 15 | attribute :status, Symbol, default: :new 16 | attribute :type 17 | attribute :analyser 18 | 19 | FLAY_DOCS_URL = 'http://docs.seattlerb.org/flay/'.freeze 20 | FLOG_DOCS_URL = 'http://docs.seattlerb.org/flog/'.freeze 21 | 22 | def at_location?(other_location) 23 | locations.any?(other_location) 24 | end 25 | 26 | def multiple_locations? 27 | locations.length > 1 28 | end 29 | 30 | def ==(other) 31 | state == other.state 32 | end 33 | alias eql? == 34 | 35 | def to_s 36 | "(#{type}) #{context} #{message}" 37 | end 38 | 39 | def to_h 40 | { 41 | context: context, 42 | cost: cost, 43 | locations: locations, 44 | message: message, 45 | score: score, 46 | status: status, 47 | type: type 48 | } 49 | end 50 | 51 | def to_json(*options) 52 | to_h.to_json(*options) 53 | end 54 | 55 | def doc_url 56 | case analyser 57 | when 'reek' 58 | "https://github.com/troessner/reek/blob/master/docs/#{dasherized_type}.md" 59 | when 'flay' 60 | FLAY_DOCS_URL 61 | when 'flog' 62 | FLOG_DOCS_URL 63 | else 64 | raise "No documentation URL set for analyser '#{analyser}' smells" 65 | end 66 | end 67 | 68 | def hash 69 | state.hash 70 | end 71 | 72 | protected 73 | 74 | def state 75 | [context, message, score, type] 76 | end 77 | 78 | private 79 | 80 | def dasherized_type 81 | type.gsub(/(?gmailcom | http://flesler.blogspot.com 3 | * Licensed under MIT 4 | * @author Ariel Flesler 5 | * @version 2.1.2 6 | */ 7 | ;(function(f){"use strict";"function"===typeof define&&define.amd?define(["jquery"],f):"undefined"!==typeof module&&module.exports?module.exports=f(require("jquery")):f(jQuery)})(function($){"use strict";function n(a){return!a.nodeName||-1!==$.inArray(a.nodeName.toLowerCase(),["iframe","#document","html","body"])}function h(a){return $.isFunction(a)||$.isPlainObject(a)?a:{top:a,left:a}}var p=$.scrollTo=function(a,d,b){return $(window).scrollTo(a,d,b)};p.defaults={axis:"xy",duration:0,limit:!0};$.fn.scrollTo=function(a,d,b){"object"=== typeof d&&(b=d,d=0);"function"===typeof b&&(b={onAfter:b});"max"===a&&(a=9E9);b=$.extend({},p.defaults,b);d=d||b.duration;var u=b.queue&&1=f[g]?0:Math.min(f[g],n));!a&&1 2 | 3 |
4 |
5 | 6 | <% if @analysed_module.committed_at %> 7 | Updated <%= timeago_tag(@analysed_module.committed_at) %> 8 | <% else %> 9 | Never committed 10 | <% end %> 11 | 12 |
13 |
14 |

<%= @analysed_module.file_location %> / <%= @analysed_module.file_name %>

15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 | <%= @analysed_module.rating %> 26 |
27 |
28 |
29 |
30 |
<%= @analysed_module.line_count %> lines of codes
31 |
<%= @analysed_module.methods_count %> methods
32 |
33 |
34 |
<%= @analysed_module.complexity_per_method %> complexity/method
35 |
<%= @analysed_module.churn %> churn
36 |
37 |
38 |
<%= @analysed_module.complexity %> complexity
39 |
<%= @analysed_module.duplication %> duplications
40 |
41 |
42 |
43 |
44 | 57 |
58 | 59 | <%= yield %> 60 |
61 | 62 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/templates/code_index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Code

5 |
6 | 7 |
8 | 9 | 10 | 11 | <% unless Config.suppress_ratings %> 12 | 13 | <% end %> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <% @analysed_modules.each do |analysed_module| %> 23 | 24 | <% unless Config.suppress_ratings %> 25 | 40 | <% end %> 41 | 48 | 49 | 50 | 51 | 52 | 53 | <% end %> 54 | 55 |
RatingNameChurnComplexityDuplicationSmells
26 | <% if Config.build_mode? %> 27 | <% master_analysed_module = Config.base_branch_collection.find(analysed_module.pathname) %> 28 | <% if !master_analysed_module %> 29 | 30 | <% elsif master_analysed_module.cost > analysed_module.cost %> 31 | 32 | <% elsif master_analysed_module.cost < analysed_module.cost %> 33 | 34 | <% else %> 35 | 36 | <% end %> 37 | <% end %> 38 |
<%= analysed_module.rating %>
39 |
42 | 47 | <%= analysed_module.churn %><%= analysed_module.complexity %><%= analysed_module.duplication %><%= analysed_module.smells_count %>
56 |
57 |
58 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/templates/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ruby Critic - Home 7 | 8 | 9 | 10 | 11 | " media="screen, projection, print" rel="stylesheet" type="text/css"> 12 | " media="screen, projection, print" rel="stylesheet" type="text/css"> 13 | " media="screen, projection, print" rel="stylesheet" type="text/css"> 14 | " media="screen, projection, print" rel="stylesheet" type="text/css"> 15 | 16 | 17 | 18 | 29 |
30 | 31 | 47 | 48 |
49 |
50 | <%= yield %> 51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/templates/line.html.erb: -------------------------------------------------------------------------------- 1 | <%= @text %> 2 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/templates/overview.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |

Overview

8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | <% if Config.source_control_present? %> 24 |
25 | <% else %> 26 |
We can't show you Churn-vs-Complexity graph because this project does not seem to be under a source code management system like git or perforce at the moment. This doesn't mean that anything is wrong, it just means certain features like this one are not available.
27 | <% end %> 28 | 29 | 33 |
34 |
35 | 36 | 37 | 38 |
39 |
40 |

Summary

41 | <% ratings = { 'A' => 'Green_DR', 'B' => 'Green_Light', 'C' => 'Yellow_Color', 42 | 'D' => 'Orange_Color', 'F' => 'Red_Color' } %> 43 |
44 | <% ratings.each do |rating, color| %> 45 |
46 |
47 |
48 |

<%= rating %>

49 |
50 |
51 |
    52 |
  • <%= @summary[rating][:files] %>

    files
  • 53 |
  • <%= @summary[rating][:churns] %>

    churns
  • 54 |
  • <%= @summary[rating][:smells] %>

    smells
  • 55 |
56 |
57 |
58 |
59 | <% end %> 60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 | 69 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/templates/simple_cov_index.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |

Coverage

8 |
9 | 10 |
11 | 12 | 13 | 14 | <% unless Config.suppress_ratings %> 15 | 16 | <% end %> 17 | 18 | 19 | 20 | 21 | <% @analysed_modules.each do |analysed_module| %> 22 | 23 | <% unless Config.suppress_ratings %> 24 | 29 | <% end %> 30 | 39 | 40 | 41 | <% end %> 42 |
RatingNameCoverage
25 |
26 | <%= analysed_module.coverage_rating %> 27 |
28 |
31 | 38 | <%= '%.2f' % analysed_module.coverage %>%
43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/templates/smells_index.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |

Smells

8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% @smells.each do |smell| %> 20 | 21 | 22 | 31 | 40 | 41 | <% end %> 42 |
SmellLocationsStatus
<%= smell.type %> 23 | 30 | 32 | <% if @show_status %> 33 | 38 | <% end %> 39 |
43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/templates/smelly_line.html.erb: -------------------------------------------------------------------------------- 1 | <%= @text %> 2 | 3 |
    4 | <% @smells.each do |smell| %> 5 |
  1. 6 |
    7 |
    8 | 9 | 10 | <%= smell.type %> 11 | 12 |
    13 | <%= "#{smell.context} #{smell.message}" %> 14 | <% if smell.multiple_locations? %> 15 | Locations: 16 | <% smell.locations.each_with_index do |location, index| %> 17 | <%= index %> 18 | <% end %> 19 | <% end %> 20 |
    21 |
  2. 22 | <% end %> 23 |
24 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/turbulence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | module RubyCritic 5 | module Turbulence 6 | def self.data(analysed_modules) 7 | analysed_modules.map do |analysed_module| 8 | { 9 | name: analysed_module.name, 10 | x: analysed_module.churn, 11 | y: analysed_module.complexity, 12 | rating: analysed_module.rating.to_s 13 | } 14 | end.to_json 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html/view_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module ViewHelpers 5 | def timeago_tag(time) 6 | "" 7 | end 8 | 9 | def asset_path(file) 10 | relative_path("assets/#{file}") 11 | end 12 | 13 | def file_path(file) 14 | relative_path(file) 15 | end 16 | 17 | def smell_location_path(location) 18 | smell_location = "#{location.pathname.sub_ext('.html')}#L#{location.line}" 19 | if Config.compare_branches_mode? 20 | file_path("#{File.expand_path(Config.feature_root_directory)}/#{smell_location}") 21 | else 22 | file_path(smell_location) 23 | end 24 | end 25 | 26 | def code_index_path(root_directory, file_name) 27 | root_directory_path = File.expand_path(root_directory) 28 | index_path = "#{root_directory_path}/#{file_name}" 29 | index_path = "#{root_directory_path}/overview.html" unless File.exist?(index_path) 30 | file_path(index_path) 31 | end 32 | 33 | private 34 | 35 | def relative_path(file) 36 | (root_directory + file).relative_path_from(file_directory) 37 | end 38 | 39 | def file_directory 40 | raise NotImplementedError, 41 | "The #{self.class} class must implement the #{__method__} method." 42 | end 43 | 44 | def root_directory 45 | raise NotImplementedError, 46 | "The #{self.class} class must implement the #{__method__} method." 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/html_report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | require 'rubycritic/configuration' 5 | require 'rubycritic/generators/html/overview' 6 | require 'rubycritic/generators/html/smells_index' 7 | require 'rubycritic/generators/html/code_index' 8 | require 'rubycritic/generators/html/simple_cov_index' 9 | require 'rubycritic/generators/html/code_file' 10 | 11 | module RubyCritic 12 | module Generator 13 | class HtmlReport 14 | ASSETS_DIR = File.expand_path('html/assets', __dir__) 15 | 16 | def initialize(analysed_modules) 17 | @analysed_modules = analysed_modules 18 | end 19 | 20 | def generate_report 21 | create_directories_and_files 22 | copy_assets_to_report_directory 23 | puts "New critique at #{report_location}" 24 | browser.open unless Config.no_browser 25 | end 26 | 27 | def browser 28 | @browser ||= RubyCritic::Browser.new(report_location) 29 | end 30 | 31 | private 32 | 33 | def create_directories_and_files 34 | Array(generators).each do |generator| 35 | FileUtils.mkdir_p(generator.file_directory) 36 | File.write(generator.file_pathname, generator.render) 37 | end 38 | end 39 | 40 | def generators 41 | [overview_generator, code_index_generator, smells_index_generator, simple_cov_index_generator] + file_generators 42 | end 43 | 44 | def overview_generator 45 | @overview_generator ||= Html::Overview.new(@analysed_modules) 46 | end 47 | 48 | def code_index_generator 49 | Html::CodeIndex.new(@analysed_modules) 50 | end 51 | 52 | def smells_index_generator 53 | Html::SmellsIndex.new(@analysed_modules) 54 | end 55 | 56 | def simple_cov_index_generator 57 | Html::SimpleCovIndex.new(@analysed_modules) 58 | end 59 | 60 | def file_generators 61 | @analysed_modules.map do |analysed_module| 62 | Html::CodeFile.new(analysed_module) 63 | end 64 | end 65 | 66 | def copy_assets_to_report_directory 67 | FileUtils.cp_r(ASSETS_DIR, Config.root) 68 | end 69 | 70 | def report_location 71 | overview_generator.file_href 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/json/simple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'rubycritic/version' 5 | require 'pathname' 6 | 7 | module RubyCritic 8 | module Generator 9 | module Json 10 | class Simple 11 | def initialize(analysed_modules) 12 | @analysed_modules = analysed_modules 13 | end 14 | 15 | FILE_NAME = 'report.json'.freeze 16 | 17 | def render 18 | JSON.dump(data) 19 | end 20 | 21 | def data 22 | { 23 | metadata: { 24 | rubycritic: { 25 | version: RubyCritic::VERSION 26 | } 27 | }, 28 | analysed_modules: @analysed_modules, 29 | score: @analysed_modules.score 30 | } 31 | end 32 | 33 | def file_directory 34 | @file_directory ||= Pathname.new(Config.root) 35 | end 36 | 37 | def file_pathname 38 | Pathname.new(file_directory).join FILE_NAME 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/json_report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/generators/json/simple' 4 | 5 | module RubyCritic 6 | module Generator 7 | class JsonReport 8 | def initialize(analysed_modules) 9 | @analysed_modules = analysed_modules 10 | end 11 | 12 | def generate_report 13 | FileUtils.mkdir_p(generator.file_directory) 14 | File.write(generator.file_pathname, generator.render) 15 | end 16 | 17 | private 18 | 19 | def generator 20 | Json::Simple.new(@analysed_modules) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/lint_report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/generators/text/lint' 4 | 5 | module RubyCritic 6 | module Generator 7 | class LintReport 8 | def initialize(analysed_modules) 9 | @analysed_modules = analysed_modules 10 | end 11 | 12 | def generate_report 13 | FileUtils.mkdir_p(generator.file_directory) 14 | File.write(generator.file_pathname, reports.join("\n")) 15 | end 16 | 17 | def generator 18 | Text::Lint 19 | end 20 | 21 | def reports 22 | @analysed_modules.sort.map do |mod| 23 | generator.new(mod).render 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/text/lint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | module RubyCritic 5 | module Generator 6 | module Text 7 | class Lint 8 | class << self 9 | TEMPLATE_PATH = File.expand_path('templates/lint.erb', __dir__) 10 | FILE_NAME = 'lint.txt'.freeze 11 | 12 | def file_directory 13 | @file_directory ||= Pathname.new(Config.root) 14 | end 15 | 16 | def file_pathname 17 | Pathname.new(file_directory).join FILE_NAME 18 | end 19 | 20 | def erb_template 21 | @erb_template ||= ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-') 22 | end 23 | end 24 | 25 | def initialize(analysed_module) 26 | @analysed_module = analysed_module 27 | end 28 | 29 | def render 30 | erb_template.result(binding) 31 | end 32 | 33 | private 34 | 35 | def erb_template 36 | self.class.erb_template 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/text/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rainbow' 4 | 5 | module RubyCritic 6 | module Generator 7 | module Text 8 | class List 9 | class << self 10 | TEMPLATE_PATH = File.expand_path('templates/list.erb', __dir__) 11 | 12 | def erb_template 13 | @erb_template ||= ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-') 14 | end 15 | end 16 | 17 | RATING_TO_COLOR = { 18 | 'A' => :green, 19 | 'B' => :green, 20 | 'C' => :yellow, 21 | 'D' => :orange, 22 | 'F' => :red 23 | }.freeze 24 | 25 | def initialize(analysed_module) 26 | @analysed_module = analysed_module 27 | end 28 | 29 | def render 30 | erb_template.result(binding) 31 | end 32 | 33 | private 34 | 35 | def erb_template 36 | self.class.erb_template 37 | end 38 | 39 | def color 40 | @color ||= RATING_TO_COLOR[@analysed_module.rating.to_s] 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/text/templates/lint.erb: -------------------------------------------------------------------------------- 1 | <%- @analysed_module.smells.each do |smell| -%> 2 | <%= smell.locations.first.to_s %>: <%= smell.to_s %> 3 | <%- end -%> 4 | -------------------------------------------------------------------------------- /lib/rubycritic/generators/text/templates/list.erb: -------------------------------------------------------------------------------- 1 | <%= Rainbow(@analysed_module.name.to_s).foreground(color) %>: 2 | Rating: <%= Rainbow(@analysed_module.rating.to_s).foreground(color) %> 3 | Churn: <%= @analysed_module.churn %> 4 | Complexity: <%= @analysed_module.complexity %> 5 | Duplication: <%= @analysed_module.duplication %> 6 | Smells: <%= @analysed_module.smells.count %> 7 | <%- @analysed_module.smells.each do |smell| -%> 8 | * <%= smell %> 9 | <%- smell.locations.each do |location| -%> 10 | - <%= location %> 11 | <%- end -%> 12 | <%- end -%> 13 | -------------------------------------------------------------------------------- /lib/rubycritic/rake_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake' 4 | require 'rake/tasklib' 5 | require 'English' 6 | require 'rubycritic/cli/application' 7 | 8 | module RubyCritic 9 | # 10 | # A rake task that runs RubyCritic on a set of source files. 11 | # 12 | # This will create a task that can be run with: 13 | # 14 | # rake rubycritic 15 | # 16 | # Example: 17 | # 18 | # require 'rubycritic/rake_task' 19 | # 20 | # RubyCritic::RakeTask.new do |task| 21 | # task.paths = FileList['lib/**/*.rb', 'spec/**/*.rb'] 22 | # end 23 | # 24 | class RakeTask < ::Rake::TaskLib 25 | # Name of RubyCritic task. Defaults to :rubycritic. 26 | attr_writer :name 27 | 28 | # Glob pattern to match source files. Defaults to FileList['.']. 29 | attr_writer :paths 30 | 31 | # Use verbose output. If this is set to true, the task will print 32 | # the rubycritic command to stdout. Defaults to false. 33 | attr_writer :verbose 34 | 35 | # You can pass all the options here in that are shown by "rubycritic -h" except for 36 | # "-p / --path" since that is set separately. Defaults to ''. 37 | attr_writer :options 38 | 39 | # Whether or not to fail Rake task when RubyCritic does not pass. 40 | # Defaults to true. 41 | attr_writer :fail_on_error 42 | 43 | def initialize(name = :rubycritic, description = 'Run RubyCritic') 44 | @name = name 45 | @description = description 46 | @paths = FileList['.'] 47 | @options = '' 48 | @verbose = false 49 | @fail_on_error = true 50 | 51 | yield self if block_given? 52 | define_task 53 | end 54 | 55 | private 56 | 57 | attr_reader :name, :description, :paths, :verbose, :options, :fail_on_error 58 | 59 | def define_task 60 | desc description 61 | task(name) { run_task } 62 | end 63 | 64 | def run_task 65 | print_starting_up_output if verbose 66 | application = RubyCritic::Cli::Application.new(options_as_arguments + paths) 67 | return unless application.execute.nonzero? && fail_on_error 68 | 69 | abort('RubyCritic did not pass - exiting!') 70 | end 71 | 72 | def print_starting_up_output 73 | puts "\n\n!!! Running `#{name}` rake command\n" 74 | puts "!!! Inspecting #{paths} #{options.empty? ? '' : "with options #{options}"}\n\n" 75 | end 76 | 77 | def options_as_arguments 78 | options.split(/\s+/) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/rubycritic/reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module Reporter 5 | REPORT_GENERATOR_CLASS_FORMATS = %i[json console lint].freeze 6 | 7 | def self.generate_report(analysed_modules) 8 | RubyCritic::Config.formats.uniq.each do |format| 9 | report_generator_class(format).new(analysed_modules).generate_report 10 | end 11 | RubyCritic::Config.formatters.each do |formatter| 12 | report_generator_class_from_formatter(formatter).new(analysed_modules).generate_report 13 | end 14 | end 15 | 16 | def self.report_generator_class(config_format) 17 | if REPORT_GENERATOR_CLASS_FORMATS.include? config_format 18 | require "rubycritic/generators/#{config_format}_report" 19 | Generator.const_get("#{config_format.capitalize}Report") 20 | else 21 | require 'rubycritic/generators/html_report' 22 | Generator::HtmlReport 23 | end 24 | end 25 | 26 | def self.report_generator_class_from_formatter(formatter) 27 | require_path, class_name = formatter.sub(/([^:]):([^:])/, '\1\;\2').split('\;', 2) 28 | class_name ||= require_path 29 | require require_path unless require_path == class_name 30 | class_from_path(class_name) 31 | end 32 | 33 | def self.class_from_path(path) 34 | path.split('::').inject(Object) { |obj, klass| obj.const_get klass } 35 | rescue NameError => error 36 | raise "Could not create reporter for class #{path}. Error: #{error}!" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/rubycritic/revision_comparator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubycritic/serializer' 4 | require 'rubycritic/analysers_runner' 5 | require 'rubycritic/smells_status_setter' 6 | require 'rubycritic/version' 7 | require 'digest/md5' 8 | 9 | module RubyCritic 10 | class RevisionComparator 11 | SNAPSHOTS_DIR_NAME = 'snapshots'.freeze 12 | 13 | def initialize(paths) 14 | @paths = paths 15 | end 16 | 17 | def statuses=(analysed_modules_now) 18 | return unless Config.source_control_system.revision? 19 | 20 | analysed_modules_before = load_cached_analysed_modules 21 | return unless analysed_modules_before 22 | 23 | SmellsStatusSetter.set( 24 | analysed_modules_before.flat_map(&:smells), 25 | analysed_modules_now.flat_map(&:smells) 26 | ) 27 | end 28 | 29 | private 30 | 31 | def load_cached_analysed_modules 32 | Serializer.new(revision_file).load if File.file?(revision_file) 33 | end 34 | 35 | def revision_file 36 | @revision_file ||= File.join( 37 | Config.root, 38 | SNAPSHOTS_DIR_NAME, 39 | VERSION, 40 | Digest::MD5.hexdigest(Marshal.dump(@paths)), 41 | Config.source_control_system.head_reference 42 | ) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/rubycritic/serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | module RubyCritic 6 | class Serializer 7 | def initialize(file) 8 | @file = file 9 | end 10 | 11 | def load 12 | Marshal.load(File.binread(@file)) 13 | end 14 | 15 | def dump(content) 16 | create_file_directory 17 | File.open(@file, 'w+') do |file| 18 | Marshal.dump(content, file) 19 | end 20 | end 21 | 22 | private 23 | 24 | def create_file_directory 25 | FileUtils.mkdir_p(file_directory) 26 | end 27 | 28 | def file_directory 29 | File.dirname(@file) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/rubycritic/smells_status_setter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module SmellsStatusSetter 5 | def self.set(smells_before, smells_now) 6 | old_smells = smells_now & smells_before 7 | set_status(old_smells, :old) 8 | new_smells = smells_now - smells_before 9 | set_status(new_smells, :new) 10 | end 11 | 12 | def self.set_status(smells, status) 13 | smells.each { |smell| smell.status = status } 14 | end 15 | 16 | private_class_method :set_status 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rubycritic/source_control_systems/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'English' 4 | require 'shellwords' 5 | 6 | module RubyCritic 7 | module SourceControlSystem 8 | class Base 9 | @@systems = [] # rubocop:disable Style/ClassVars 10 | 11 | def self.register_system 12 | @@systems << self 13 | end 14 | 15 | def self.systems 16 | @@systems 17 | end 18 | 19 | def self.create 20 | supported_system = systems.find(&:supported?) 21 | if supported_system 22 | supported_system.new 23 | else 24 | puts 'RubyCritic can provide more feedback if you use ' \ 25 | "a #{connected_system_names} repository. " \ 26 | 'Churn will not be calculated.' 27 | Double.new 28 | end 29 | end 30 | 31 | def self.connected_system_names 32 | "#{systems[0...-1].join(', ')} or #{systems[-1]}" 33 | end 34 | end 35 | end 36 | end 37 | 38 | require 'rubycritic/source_control_systems/double' 39 | require 'rubycritic/source_control_systems/git' 40 | require 'rubycritic/source_control_systems/mercurial' 41 | require 'rubycritic/source_control_systems/perforce' 42 | -------------------------------------------------------------------------------- /lib/rubycritic/source_control_systems/double.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module SourceControlSystem 5 | class Double < Base 6 | def revisions_count(_path) 7 | 0 8 | end 9 | 10 | def date_of_last_commit(_path) 11 | nil 12 | end 13 | 14 | def revision? 15 | false 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rubycritic/source_control_systems/git.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tty/which' 4 | require 'rubycritic/source_control_systems/git/churn' 5 | 6 | module RubyCritic 7 | module SourceControlSystem 8 | class Git < Base 9 | register_system 10 | GIT_EXECUTABLE = TTY::Which.which('git') 11 | 12 | def self.git(arg) 13 | if Gem.win_platform? 14 | `\"#{GIT_EXECUTABLE}\" #{arg}` 15 | else 16 | `#{GIT_EXECUTABLE} #{arg}` 17 | end 18 | end 19 | 20 | def git(arg) 21 | self.class.git(arg) 22 | end 23 | 24 | def self.supported? 25 | git('branch 2>&1') && $CHILD_STATUS.success? 26 | end 27 | 28 | def self.to_s 29 | 'Git' 30 | end 31 | 32 | def churn 33 | @churn ||= Churn.new(churn_after: Config.churn_after, paths: Config.paths) 34 | end 35 | 36 | def revisions_count(path) 37 | churn.revisions_count(path) 38 | end 39 | 40 | def date_of_last_commit(path) 41 | churn.date_of_last_commit(path) 42 | end 43 | 44 | def revision? 45 | head_reference && $CHILD_STATUS.success? 46 | end 47 | 48 | def head_reference 49 | git('rev-parse --verify HEAD').chomp! 50 | end 51 | 52 | def travel_to_head 53 | stash_successful = stash_changes 54 | yield 55 | ensure 56 | travel_to_original_state if stash_successful 57 | end 58 | 59 | def self.switch_branch(branch) 60 | dirty = !uncommitted_changes.empty? 61 | abort("Uncommitted changes are present: #{uncommitted_changes}") if dirty 62 | 63 | git("checkout #{branch}") 64 | end 65 | 66 | def self.uncommitted_changes 67 | return @uncommitted_changes if defined? @uncommitted_changes 68 | 69 | @uncommitted_changes = git('diff-index HEAD --').chomp! || '' 70 | end 71 | 72 | def self.modified_files 73 | modified_files = `git diff --name-status #{Config.base_branch} #{Config.feature_branch}` 74 | modified_files.split("\n").filter_map do |line| 75 | next if line.start_with?('D') 76 | 77 | file_name = line.split("\t")[1] 78 | file_name 79 | end 80 | end 81 | 82 | def self.current_branch 83 | branch_list = `git branch` 84 | branch = branch_list.match(/\*.*$/)[0].gsub('* ', '') 85 | branch = branch.gsub(/\(HEAD detached at (.*)\)$/, '\1') if /\(HEAD detached at (.*)\)$/.match?(branch) 86 | branch 87 | end 88 | 89 | private 90 | 91 | def stash_changes 92 | stashes_count_before = stashes_count 93 | git('stash') 94 | stashes_count_after = stashes_count 95 | stashes_count_after > stashes_count_before 96 | end 97 | 98 | def stashes_count 99 | git('stash list --format=%h').count("\n") 100 | end 101 | 102 | def travel_to_original_state 103 | git('stash pop') 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/rubycritic/source_control_systems/git/churn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module SourceControlSystem 5 | class Git < Base 6 | Stats = Struct.new(:count, :date) 7 | 8 | class Renames 9 | def initialize 10 | @data = {} 11 | end 12 | 13 | def renamed(from, to) 14 | current = current(to) 15 | @data[from] = current 16 | end 17 | 18 | def current(name) 19 | @data.fetch(name, name) 20 | end 21 | end 22 | 23 | class Churn 24 | def initialize(churn_after: nil, paths: ['.']) 25 | @churn_after = churn_after 26 | @paths = Array(paths) 27 | @date = nil 28 | @stats = {} 29 | 30 | call 31 | end 32 | 33 | def revisions_count(path) 34 | stats(path).count 35 | end 36 | 37 | def date_of_last_commit(path) 38 | stats(path).date 39 | end 40 | 41 | private 42 | 43 | def call 44 | git_log_commands.each { |log_command| exec_git_command(log_command) } 45 | end 46 | 47 | def exec_git_command(command) 48 | Git 49 | .git(command) 50 | .split("\n") 51 | .reject(&:empty?) 52 | .each { |line| process_line(line) } 53 | end 54 | 55 | def git_log_commands 56 | @paths.map { |path| git_log_command(path) } 57 | end 58 | 59 | def git_log_command(path) 60 | "log --all --date=iso --follow --format='format:date:%x09%ad' --name-status #{after_clause}#{path}" 61 | end 62 | 63 | def after_clause 64 | @churn_after ? "--after='#{@churn_after}' " : '' 65 | end 66 | 67 | def process_line(line) 68 | operation, *rest = line.split("\t") 69 | 70 | case operation 71 | when /^date:/ 72 | process_date(*rest) 73 | when /^[RC]/ 74 | process_rename(*rest) 75 | else 76 | rest = filename_for_subdirectory(rest[0]) 77 | process_file(*rest) 78 | end 79 | end 80 | 81 | def process_date(date) 82 | @date = date 83 | end 84 | 85 | def process_rename(from, to) 86 | renames.renamed(from, to) 87 | process_file(to) 88 | end 89 | 90 | def filename_for_subdirectory(filename) 91 | git_path = Git.git('rev-parse --show-toplevel') 92 | cd_path = Dir.pwd 93 | if cd_path.length > git_path.length 94 | filename = filename.sub(/^#{Regexp.escape("#{File.basename(cd_path)}/")}/, '') 95 | end 96 | [filename] 97 | end 98 | 99 | def process_file(filename) 100 | record_commit(renames.current(filename), @date) 101 | end 102 | 103 | def record_commit(filename, date) 104 | stats = @stats[filename] ||= Stats.new(0, date) 105 | stats.count += 1 106 | end 107 | 108 | def renames 109 | @renames ||= Renames.new 110 | end 111 | 112 | def stats(path) 113 | @stats.fetch(path, Stats.new(0)) 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/rubycritic/source_control_systems/mercurial.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | module SourceControlSystem 5 | class Mercurial < Base 6 | register_system 7 | 8 | def self.supported? 9 | hg_verify = `hg verify 2>&1` 10 | hg_verify && $CHILD_STATUS.success? 11 | end 12 | 13 | def self.to_s 14 | 'Mercurial' 15 | end 16 | 17 | def revisions_count(path) 18 | `hg log #{path.shellescape} --template '1'`.size 19 | end 20 | 21 | def date_of_last_commit(path) 22 | `hg log #{path.shellescape} --template '{date|isodate}' --limit 1`.chomp 23 | end 24 | 25 | def revision? 26 | false 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rubycritic/source_locator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | require 'rubycritic/configuration' 5 | 6 | module RubyCritic 7 | class SourceLocator 8 | RUBY_EXTENSION = '.rb'.freeze 9 | RUBY_FILES = File.join('**', "*#{RUBY_EXTENSION}") 10 | RUBY_SHEBANG = '#!/usr/bin/env ruby'.freeze 11 | 12 | def initialize(paths) 13 | @initial_paths = Array(paths) 14 | end 15 | 16 | def paths 17 | @paths ||= pathnames.map(&:to_s) 18 | end 19 | 20 | def pathnames 21 | @pathnames ||= expand_paths 22 | end 23 | 24 | private 25 | 26 | def deduplicate_symlinks(path_list) 27 | # sort the symlinks to the end so files are preferred 28 | path_list.sort_by! { |path| File.symlink?(path.cleanpath) ? 'z' : 'a' } 29 | if defined?(JRUBY_VERSION) 30 | require 'java' 31 | path_list.uniq! do |path| 32 | java.io.File.new(path.realpath.to_s).canonical_path 33 | end 34 | else 35 | path_list.uniq!(&:realpath) 36 | end 37 | end 38 | 39 | def expand_paths 40 | path_list = @initial_paths.flat_map do |path| 41 | if File.directory?(path) 42 | Pathname.glob(File.join(path, RUBY_FILES)) 43 | elsif File.exist?(path) && ruby_file?(path) 44 | Pathname.new(path) 45 | end 46 | end.compact 47 | 48 | deduplicate_symlinks(path_list) if Config.deduplicate_symlinks 49 | 50 | path_list.map(&:cleanpath) 51 | end 52 | 53 | def ruby_file?(path) 54 | Config.ruby_extensions.include?(File.extname(path)) || File.open(path, &:gets).to_s.match?(RUBY_SHEBANG) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/rubycritic/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyCritic 4 | VERSION = '4.9.2' 5 | end 6 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | Check list: 2 | - [ ] Add an entry to the [changelog](https://github.com/whitesmith/rubycritic/blob/main/CHANGELOG.md) 3 | - [ ] [Squash all commits into a single one](https://github.com/whitesmith/rubycritic/blob/main/CONTRIBUTING.md) 4 | - [ ] Describe your PR, link issues, etc. 5 | -------------------------------------------------------------------------------- /rubycritic.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'rubycritic/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'rubycritic' 9 | spec.version = RubyCritic::VERSION 10 | spec.authors = ['Guilherme Simoes'] 11 | spec.email = ['guilherme.rdems@gmail.com'] 12 | spec.description = 'RubyCritic is a tool that wraps around various static analysis gems ' \ 13 | 'to provide a quality report of your Ruby code.' 14 | spec.summary = 'RubyCritic is a Ruby code quality reporter' 15 | spec.homepage = 'https://github.com/whitesmith/rubycritic' 16 | spec.license = 'MIT' 17 | spec.required_ruby_version = '>= 3.1.0' 18 | 19 | spec.files = [ 20 | 'CHANGELOG.md', 21 | 'CONTRIBUTING.md', 22 | 'Gemfile', 23 | 'LICENSE.txt', 24 | 'README.md', 25 | 'ROADMAP.md', 26 | 'Rakefile' 27 | ] 28 | spec.files += `git ls-files lib`.split("\n") 29 | 30 | spec.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 31 | spec.require_path = 'lib' 32 | 33 | spec.add_dependency 'flay', '~> 2.13' 34 | spec.add_dependency 'flog', '~> 4.7' 35 | spec.add_dependency 'launchy', '>= 2.5.2' 36 | spec.add_dependency 'parser', '>= 3.3.0.5' 37 | spec.add_dependency 'rainbow', '~> 3.1.1' 38 | spec.add_dependency 'reek', '~> 6.4.0', '< 7.0' 39 | spec.add_dependency 'rexml' 40 | spec.add_dependency 'ruby_parser', '~> 3.21' 41 | spec.add_dependency 'simplecov', '>= 0.22.0' 42 | spec.add_dependency 'tty-which', '~> 0.5.0' 43 | spec.add_dependency 'virtus', '~> 2.0' 44 | 45 | spec.add_development_dependency 'aruba', '~> 2.3.0' 46 | spec.add_development_dependency 'bundler', '>= 2.0.0' 47 | if RUBY_PLATFORM == 'java' 48 | spec.add_development_dependency 'pry-debugger-jruby' 49 | else 50 | spec.add_development_dependency 'byebug', '~> 11.0', '>= 10.0' 51 | end 52 | spec.add_development_dependency 'cucumber', '~> 9.2.1', '!= 9.0.0' 53 | spec.add_development_dependency 'diff-lcs', '~> 1.3' 54 | spec.add_development_dependency 'fakefs', '~> 2.6.0' 55 | spec.add_development_dependency 'irb' 56 | spec.add_development_dependency 'mdl', '~> 0.13.0', '>= 0.12.0' 57 | spec.add_development_dependency 'minitest', '~> 5.25.2', '>= 5.3.0' 58 | spec.add_development_dependency 'minitest-around', '~> 0.5.0', '>= 0.4.0' 59 | spec.add_development_dependency 'mocha', '~> 2.7.1' 60 | spec.add_development_dependency 'ostruct' 61 | spec.add_development_dependency 'rake', '~> 13.2.0', '>= 11.0.0' 62 | spec.add_development_dependency 'rdoc' 63 | spec.add_development_dependency 'rexml', '>= 3.2.0' 64 | spec.add_development_dependency 'rubocop', '>= 1.72.0', '< 2.0' 65 | spec.add_development_dependency 'rubocop-minitest' 66 | spec.add_development_dependency 'rubocop-performance' 67 | spec.add_development_dependency 'rubocop-rake' 68 | spec.metadata['rubygems_mfa_required'] = 'true' 69 | end 70 | -------------------------------------------------------------------------------- /test/analysers_test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class AnalysedModuleDouble < OpenStruct; end 6 | 7 | require_relative '../lib/rubycritic/core/analysed_modules_collection' 8 | class AnalysedModulesCollectionDouble < RubyCritic::AnalysedModulesCollection 9 | def initialize(module_doubles) 10 | @modules = module_doubles 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fakefs_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'etc' 4 | require 'fakefs/safe' 5 | 6 | module FakeFS 7 | class File < StringIO 8 | # $VERBOSE = nil to suppress warnings when we override flock. 9 | original_verbose = $VERBOSE 10 | $VERBOSE = nil 11 | def flock(*) 12 | true 13 | end 14 | $VERBOSE = original_verbose 15 | end 16 | end 17 | 18 | module FakeFSPatch 19 | def home(user = Etc.getlogin) 20 | RealDir.home(user) 21 | end 22 | end 23 | FakeFS::Dir.singleton_class.prepend(FakeFSPatch) 24 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/churn_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'analysers_test_helper' 4 | require 'rubycritic/analysers/churn' 5 | require 'rubycritic/source_control_systems/base' 6 | 7 | describe RubyCritic::Analyser::Churn do 8 | context 'when analysing a file' do 9 | before do 10 | @analysed_module = AnalysedModuleDouble.new(path: 'path_to_some_file.rb') 11 | analysed_modules = [@analysed_module] 12 | analyser = RubyCritic::Analyser::Churn.new(analysed_modules) 13 | analyser.source_control_system = SourceControlSystemDouble.new 14 | analyser.run 15 | end 16 | 17 | it 'calculates its churn' do 18 | _(@analysed_module.churn).must_equal 1 19 | end 20 | 21 | it 'determines the date of its last commit' do 22 | _(@analysed_module.committed_at).must_equal '2013-10-09 12:52:49 +0100' 23 | end 24 | end 25 | end 26 | 27 | class SourceControlSystemDouble < RubyCritic::SourceControlSystem::Base 28 | def revisions_count(_path) 29 | 1 # churn 30 | end 31 | 32 | def date_of_last_commit(_path) 33 | '2013-10-09 12:52:49 +0100' 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/complexity_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'analysers_test_helper' 4 | require 'rubycritic/analysers/complexity' 5 | 6 | describe RubyCritic::Analyser::Complexity do 7 | context 'when analysing a file' do 8 | before do 9 | @analysed_module = AnalysedModuleDouble.new(path: 'test/samples/flog/complex.rb', smells: []) 10 | analysed_modules = [@analysed_module] 11 | RubyCritic::Analyser::Complexity.new(analysed_modules).run 12 | end 13 | 14 | it 'calculates its complexity' do 15 | _(@analysed_module.complexity).must_be :>, 0 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/coverage_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'analysers_test_helper' 4 | require 'rubycritic/analysers/coverage' 5 | require 'rubycritic/source_control_systems/base' 6 | require 'fakefs_helper' 7 | 8 | describe RubyCritic::Analyser::Coverage do 9 | describe '#run' do 10 | let(:simplecov_version_dir) do 11 | if Gem.loaded_specs['simplecov'].version >= Gem::Version.new('0.21') 12 | '0.21' 13 | else 14 | '0.18' 15 | end 16 | end 17 | let(:coverage_path) do 18 | File.join(PathHelper.project_path, 'test', 'samples', 'coverage_sample', simplecov_version_dir) 19 | end 20 | let(:resultset_file) do 21 | File.join(coverage_path, '.resultset.json') 22 | end 23 | let(:old_content) do 24 | File.read(resultset_file) 25 | end 26 | 27 | before do 28 | with_cloned_fs do 29 | SimpleCov.root(PathHelper.project_path) 30 | new_content = old_content.gsub('[REPLACE_ME]', PathHelper.project_path) 31 | File.write(resultset_file, new_content) 32 | SimpleCov.coverage_dir(coverage_path) 33 | @analysed_module = AnalysedModuleDouble.new(path: path) 34 | @analysed_modules = [@analysed_module] 35 | analyser = RubyCritic::Analyser::Coverage.new(@analysed_modules) 36 | analyser.run 37 | end 38 | end 39 | 40 | context 'when analysing a file with no test coverage' do 41 | let(:path) { 'some_file.rb' } 42 | 43 | it 'calculates its test coverage as 0' do 44 | _(@analysed_module.coverage).must_equal 0 45 | end 46 | end 47 | 48 | context 'when analysing a file with some test coverage' do 49 | let(:path) { 'lib/rubycritic/source_control_systems/double.rb' } 50 | 51 | it 'calculates its test coverage' do 52 | _(@analysed_module.coverage).must_equal 75 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/helpers/methods_counter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'analysers_test_helper' 4 | require 'rubycritic/analysers/helpers/methods_counter' 5 | 6 | describe RubyCritic::MethodsCounter do 7 | describe '#count' do 8 | context 'when a file contains Ruby code' do 9 | it 'calculates the number of methods' do 10 | analysed_module = AnalysedModuleDouble.new(path: 'test/samples/methods_count.rb') 11 | _(RubyCritic::MethodsCounter.new(analysed_module).count).must_equal 2 12 | end 13 | end 14 | 15 | context 'when a file is empty' do 16 | it 'returns 0 as the number of methods' do 17 | analysed_module = AnalysedModuleDouble.new(path: 'test/samples/empty.rb') 18 | _(RubyCritic::MethodsCounter.new(analysed_module).count).must_equal 0 19 | end 20 | end 21 | 22 | context 'when a file has no method' do 23 | it 'does not blow up and returns 0 as the number of methods' do 24 | analysed_module = AnalysedModuleDouble.new(path: 'test/samples/no_methods.rb') 25 | capture_output_streams do 26 | _(RubyCritic::MethodsCounter.new(analysed_module).count).must_equal 0 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/helpers/modules_locator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/analysers/helpers/modules_locator' 5 | require 'rubycritic/core/analysed_module' 6 | require 'pathname' 7 | 8 | describe RubyCritic::ModulesLocator do 9 | describe '#names' do 10 | context 'when a file contains Ruby code' do 11 | it 'returns the names of all the classes and modules inside the file' do 12 | analysed_module = RubyCritic::AnalysedModule.new( 13 | pathname: Pathname.new('test/samples/module_names.rb'), 14 | methods_count: 1 15 | ) 16 | _(RubyCritic::ModulesLocator.new(analysed_module).names) 17 | .must_equal ['Foo', 'Foo::Bar', 'Foo::Baz', 'Foo::Qux', 'Foo::Quux::Corge'] 18 | end 19 | end 20 | 21 | context 'when a file is empty' do 22 | it 'returns the name of the file titleized' do 23 | analysed_module = RubyCritic::AnalysedModule.new( 24 | pathname: Pathname.new('test/samples/empty.rb'), 25 | methods_count: 1 26 | ) 27 | _(RubyCritic::ModulesLocator.new(analysed_module).names).must_equal ['Empty'] 28 | end 29 | end 30 | 31 | context 'when a file has no methods' do 32 | it 'returns the names of all the classes and modules inside the file' do 33 | analysed_module = RubyCritic::AnalysedModule.new( 34 | pathname: Pathname.new('test/samples/no_methods.rb'), 35 | methods_count: 0 36 | ) 37 | capture_output_streams do 38 | _(RubyCritic::ModulesLocator.new(analysed_module).names).must_equal ['Foo::NoMethods'] 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/smells/flay_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/analysers/smells/flay' 5 | require 'rubycritic/core/analysed_module' 6 | require 'pathname' 7 | 8 | describe RubyCritic::Analyser::FlaySmells do 9 | context 'when analysing a bunch of files with duplicate code' do 10 | before do 11 | @analysed_modules = [ 12 | RubyCritic::AnalysedModule.new(pathname: Pathname.new('test/samples/flay/smelly.rb')), 13 | RubyCritic::AnalysedModule.new(pathname: Pathname.new('test/samples/flay/smelly2.rb')) 14 | ] 15 | RubyCritic::Analyser::FlaySmells.new(@analysed_modules).run 16 | end 17 | 18 | it 'detects its smells' do 19 | _(@analysed_modules.first.smells?).must_equal true 20 | end 21 | 22 | it 'creates smells with messages' do 23 | smell = @analysed_modules.first.smells.first 24 | _(smell.message).must_be_instance_of String 25 | end 26 | 27 | it 'creates smells with scores' do 28 | smell = @analysed_modules.first.smells.first 29 | _(smell.score).must_be_kind_of Numeric 30 | end 31 | 32 | it 'creates smells with more than one location' do 33 | smell = @analysed_modules.first.smells.first 34 | _(smell.multiple_locations?).must_equal true 35 | end 36 | 37 | it 'calculates the mass of duplicate code' do 38 | _(@analysed_modules.first.duplication).must_be(:>, 0) 39 | end 40 | end 41 | 42 | context 'when some files are ignored using .flayignore' do 43 | before do 44 | FileUtils.ln_s('test/samples/flay/.flayignore', '.') 45 | @analysed_modules = [ 46 | RubyCritic::AnalysedModule.new(pathname: Pathname.new('test/samples/flay/smelly.rb')), 47 | RubyCritic::AnalysedModule.new(pathname: Pathname.new('test/samples/flay/smelly2.rb')) 48 | ] 49 | RubyCritic::Analyser::FlaySmells.new(@analysed_modules).run 50 | end 51 | 52 | after do 53 | FileUtils.rm(%w[.flayignore]) 54 | end 55 | 56 | it "doesn't detect smells for the ignored files" do 57 | _(@analysed_modules.first.smells?).must_equal false 58 | end 59 | 60 | it 'still detects smells for non-ignored files' do 61 | _(@analysed_modules.last.smells?).must_equal true 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/smells/flog_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'analysers_test_helper' 4 | require 'rubycritic/analysers/smells/flog' 5 | 6 | describe RubyCritic::Analyser::FlogSmells do 7 | context 'when analysing a complex file' do 8 | before do 9 | @analysed_module = AnalysedModuleDouble.new(path: 'test/samples/flog/smelly.rb', smells: []) 10 | analysed_modules = [@analysed_module] 11 | RubyCritic::Analyser::FlogSmells.new(analysed_modules).run 12 | end 13 | 14 | it 'detects its smells' do 15 | _(@analysed_module.smells.length).must_equal 1 16 | end 17 | 18 | it 'creates smells with messages' do 19 | smell = @analysed_module.smells.first 20 | _(smell.message).must_be_instance_of String 21 | end 22 | 23 | it 'creates smells with scores' do 24 | smell = @analysed_module.smells.first 25 | _(smell.score).must_be :>, 0 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysers/smells/reek_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'analysers_test_helper' 4 | require 'rubycritic/analysers/smells/reek' 5 | 6 | describe RubyCritic::Analyser::ReekSmells do 7 | context 'when analysing a smelly file' do 8 | before do 9 | pathname = Pathname.new('test/samples/reek/smelly.rb') 10 | @analysed_module = AnalysedModuleDouble.new(pathname: pathname, smells: []) 11 | analysed_modules = [@analysed_module] 12 | RubyCritic::Analyser::ReekSmells.new(analysed_modules).run 13 | end 14 | 15 | it 'detects its smells' do 16 | _(@analysed_module.smells.length).must_equal 2 17 | end 18 | 19 | it 'respects the .reek file' do 20 | messages = @analysed_module.smells.map(&:message) 21 | _(messages).wont_include "has the parameter name 'a'" 22 | end 23 | 24 | it 'creates smells with messages' do 25 | first_smell = @analysed_module.smells.first 26 | _(first_smell.message).must_equal "has boolean parameter 'reek'" 27 | 28 | last_smell = @analysed_module.smells.last 29 | _(last_smell.message).must_equal 'has no descriptive comment' 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/lib/rubycritic/analysis_summary_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/analysis_summary' 5 | 6 | module RubyCritic 7 | describe AnalysisSummary do 8 | before do 9 | analysed_modules = AnalysedModulesCollectionDouble.new( 10 | [ 11 | AnalysedModuleDouble.new(rating: 'A', churn: 2, smells: %i[a b c]), 12 | AnalysedModuleDouble.new(rating: 'A', churn: 3, smells: [:b]), 13 | AnalysedModuleDouble.new(rating: 'A', churn: 4, smells: %i[x y]), 14 | AnalysedModuleDouble.new(rating: 'B', churn: 5, smells: %i[a z]) 15 | ] 16 | ) 17 | @summary = RubyCritic::AnalysisSummary.generate(analysed_modules) 18 | end 19 | 20 | describe '.root' do 21 | it 'computes correct summary' do 22 | _(@summary['A'].to_a).must_equal({ files: 3, churns: 9, smells: 6 }.to_a) 23 | _(@summary['B'].to_a).must_equal({ files: 1, churns: 5, smells: 2 }.to_a) 24 | _(@summary['C'].to_a).must_equal({ files: 0, churns: 0, smells: 0 }.to_a) 25 | _(@summary['D'].to_a).must_equal({ files: 0, churns: 0, smells: 0 }.to_a) 26 | _(@summary['F'].to_a).must_equal({ files: 0, churns: 0, smells: 0 }.to_a) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/lib/rubycritic/browser_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/browser' 5 | 6 | describe RubyCritic::Browser do 7 | before do 8 | @report_path = 'tmp/rubycritic/overview.html' 9 | @browser = RubyCritic::Browser.new @report_path 10 | end 11 | 12 | describe '#open' do 13 | it 'should be open report with launch browser' do 14 | Launchy.stubs(:open).returns(true) 15 | _(@browser.open).must_equal true 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/lib/rubycritic/commands/status_reporter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/commands/status_reporter' 5 | require 'rubycritic/cli/options' 6 | 7 | describe RubyCritic::Command::StatusReporter do 8 | let(:success_status) { RubyCritic::Command::StatusReporter::SUCCESS } 9 | let(:score_below_minimum) { RubyCritic::Command::StatusReporter::SCORE_BELOW_MINIMUM } 10 | 11 | describe 'with default options' do 12 | before do 13 | @options = RubyCritic::Cli::Options.new([]) 14 | @options.parse 15 | @reporter = RubyCritic::Command::StatusReporter.new(@options.to_h) 16 | end 17 | 18 | it 'has a default' do 19 | _(@reporter.status).must_equal success_status 20 | _(@reporter.status_message).must_be_nil 21 | end 22 | 23 | it 'accept a score' do 24 | @reporter.score = 50.0 25 | _(@reporter.status).must_equal success_status 26 | _(@reporter.status_message).must_equal 'Score: 50.0' 27 | end 28 | 29 | it 'should format the score' do 30 | @reporter.score = 98.95258620689656 31 | _(@reporter.status).must_equal success_status 32 | _(@reporter.status_message).must_equal 'Score: 98.95' 33 | end 34 | end 35 | 36 | describe 'with minimum-score option' do 37 | before do 38 | @options = RubyCritic::Cli::Options.new(['-s', '99']) 39 | @options.parse 40 | @reporter = RubyCritic::Command::StatusReporter.new(@options.to_h) 41 | end 42 | 43 | it 'has a default' do 44 | _(@reporter.status).must_equal success_status 45 | _(@reporter.status_message).must_be_nil 46 | end 47 | 48 | describe 'when score is below minimum' do 49 | let(:score) { 98.0 } 50 | it 'should return the correct status' do 51 | @reporter.score = score 52 | _(@reporter.status).must_equal score_below_minimum 53 | _(@reporter.status_message).must_equal 'Score (98.0) is below the minimum 99.0' 54 | end 55 | 56 | it 'should format the score' do 57 | @reporter.score = 98.95258620689656 58 | _(@reporter.status).must_equal score_below_minimum 59 | _(@reporter.status_message).must_equal 'Score (98.95) is below the minimum 99.0' 60 | end 61 | end 62 | 63 | describe 'when score is equal the minimum' do 64 | let(:score) { 99.0 } 65 | it 'should return the correct status' do 66 | @reporter.score = score 67 | _(@reporter.status).must_equal success_status 68 | _(@reporter.status_message).must_equal 'Score: 99.0' 69 | end 70 | end 71 | 72 | describe 'when score is above the minimum' do 73 | let(:score) { 100.0 } 74 | it 'should return the correct status' do 75 | @reporter.score = score 76 | _(@reporter.status).must_equal success_status 77 | _(@reporter.status_message).must_equal 'Score: 100.0' 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/lib/rubycritic/configuration_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/configuration' 5 | 6 | describe RubyCritic::Configuration do 7 | describe '#root' do 8 | before do 9 | RubyCritic::Config.set 10 | @default = RubyCritic::Config.root 11 | end 12 | 13 | it 'has a default' do 14 | _(RubyCritic::Config.root).must_be_instance_of String 15 | end 16 | 17 | it 'can be set to a relative path' do 18 | RubyCritic::Config.root = 'foo' 19 | _(RubyCritic::Config.root).must_equal File.expand_path('foo') 20 | end 21 | 22 | it 'can be set to an absolute path' do 23 | RubyCritic::Config.root = '/foo' 24 | _(RubyCritic::Config.root).must_equal '/foo' 25 | end 26 | 27 | after do 28 | RubyCritic::Config.root = @default 29 | end 30 | end 31 | 32 | describe '#formats' do 33 | before do 34 | RubyCritic::Config.set(formats: []) 35 | end 36 | 37 | it 'sets html format by default' do 38 | _(RubyCritic::Config.formats).must_equal [:html] 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/lib/rubycritic/core/analysed_module_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/core/analysed_module' 5 | require 'rubycritic/core/smell' 6 | 7 | describe RubyCritic::AnalysedModule do 8 | describe 'attribute readers' do 9 | before do 10 | @name = 'Foo' 11 | @pathname = Pathname.new('foo.rb') 12 | @smells = [] 13 | @churn = 1 14 | @complexity = 2 15 | @analysed_module = RubyCritic::AnalysedModule.new( 16 | name: @name, 17 | pathname: @pathname, 18 | smells: @smells, 19 | churn: @churn, 20 | complexity: @complexity 21 | ) 22 | end 23 | 24 | it 'has a name reader' do 25 | _(@analysed_module.name).must_equal @name 26 | end 27 | 28 | it 'has a pathname reader' do 29 | _(@analysed_module.pathname).must_equal @pathname 30 | end 31 | 32 | it 'has a path reader' do 33 | _(@analysed_module.path).must_equal @pathname.to_s 34 | end 35 | 36 | it 'has a smells reader' do 37 | _(@analysed_module.smells).must_equal @smells 38 | end 39 | 40 | it 'has a churn reader' do 41 | _(@analysed_module.churn).must_equal @churn 42 | end 43 | 44 | it 'has a complexity reader' do 45 | _(@analysed_module.complexity).must_equal @complexity 46 | end 47 | end 48 | 49 | describe '#cost' do 50 | it 'returns the remediation cost of fixing the analysed_module' do 51 | smells = [SmellDouble.new(cost: 1), SmellDouble.new(cost: 2)] 52 | analysed_module = RubyCritic::AnalysedModule.new(smells: smells, complexity: 0) 53 | _(analysed_module.cost).must_equal 3 54 | end 55 | end 56 | 57 | describe '#complexity_per_method' do 58 | context 'when the file has no methods' do 59 | it 'returns a placeholder' do 60 | analysed_module = RubyCritic::AnalysedModule.new(complexity: 0, methods_count: 0) 61 | _(analysed_module.complexity_per_method).must_equal 'N/A' 62 | end 63 | end 64 | 65 | context 'when the file has at least one method' do 66 | it 'returns the average complexity per method' do 67 | analysed_module = RubyCritic::AnalysedModule.new(complexity: 10, methods_count: 2) 68 | _(analysed_module.complexity_per_method).must_equal 5 69 | end 70 | end 71 | end 72 | 73 | describe '#smells?' do 74 | it 'returns true if the analysed_module has at least one smell' do 75 | analysed_module = RubyCritic::AnalysedModule.new(smells: [SmellDouble.new]) 76 | _(analysed_module.smells?).must_equal true 77 | end 78 | end 79 | 80 | describe '#smells_at_location' do 81 | it 'returns the smells of an analysed_module at a certain location' do 82 | location = RubyCritic::Location.new('./foo', '42') 83 | smells = [RubyCritic::Smell.new(locations: [location])] 84 | analysed_module = RubyCritic::AnalysedModule.new(smells: smells) 85 | _(analysed_module.smells_at_location(location)).must_equal smells 86 | end 87 | end 88 | end 89 | 90 | class SmellDouble < OpenStruct; end 91 | -------------------------------------------------------------------------------- /test/lib/rubycritic/core/location_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/core/location' 5 | 6 | describe RubyCritic::Location do 7 | describe 'attribute readers' do 8 | before do 9 | @path = './foo.rb' 10 | @line = '42' 11 | @location = RubyCritic::Location.new(@path, @line) 12 | end 13 | 14 | it 'has a pathname' do 15 | _(@location.pathname).must_equal Pathname.new(@path) 16 | end 17 | 18 | it 'has a line number' do 19 | _(@location.line).must_equal @line.to_i 20 | end 21 | 22 | it 'has a file name' do 23 | _(@location.file_name).must_equal 'foo' 24 | end 25 | end 26 | 27 | it 'is comparable' do 28 | location1 = RubyCritic::Location.new('./foo', 42) 29 | location2 = RubyCritic::Location.new('./foo', 42) 30 | _(location1).must_equal location2 31 | end 32 | 33 | it 'is sortable' do 34 | location1 = RubyCritic::Location.new('./foo', 42) 35 | location2 = RubyCritic::Location.new('./bar', 23) 36 | location3 = RubyCritic::Location.new('./bar', 16) 37 | _([location1, location2, location3].sort).must_equal [location3, location2, location1] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/lib/rubycritic/core/smell_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/core/smell' 5 | 6 | describe RubyCritic::Smell do 7 | describe 'attribute readers' do 8 | before do 9 | @locations = [RubyCritic::Location.new('./foo', '42')] 10 | @context = '#bar' 11 | @message = 'This smells' 12 | @score = 0 13 | @type = :complexity 14 | @smell = RubyCritic::Smell.new( 15 | locations: @locations, 16 | context: @context, 17 | message: @message, 18 | score: @score, 19 | type: @type 20 | ) 21 | end 22 | 23 | it 'has a context reader' do 24 | _(@smell.context).must_equal @context 25 | end 26 | 27 | it 'has a locations reader' do 28 | _(@smell.locations).must_equal @locations 29 | end 30 | 31 | it 'has a message reader' do 32 | _(@smell.message).must_equal @message 33 | end 34 | 35 | it 'has a score reader' do 36 | _(@smell.score).must_equal @score 37 | end 38 | 39 | it 'has a type reader' do 40 | _(@smell.type).must_equal @type 41 | end 42 | end 43 | 44 | describe '#at_location?' do 45 | it 'returns true if the smell has a location that matches the location passed as argument' do 46 | location = RubyCritic::Location.new('./foo', '42') 47 | smell = RubyCritic::Smell.new(locations: [location]) 48 | _(smell.at_location?(location)).must_equal true 49 | end 50 | end 51 | 52 | describe '#multiple_locations?' do 53 | it 'returns true if the smell has more than one location' do 54 | location1 = RubyCritic::Location.new('./foo', '42') 55 | location2 = RubyCritic::Location.new('./foo', '23') 56 | smell = RubyCritic::Smell.new(locations: [location1, location2]) 57 | _(smell.multiple_locations?).must_equal true 58 | end 59 | end 60 | 61 | describe '#==' do 62 | it 'returns true if two smells have the same base attributes' do 63 | attributes = { 64 | context: '#bar', 65 | message: 'This smells', 66 | score: 0, 67 | type: :complexity 68 | } 69 | smell1 = RubyCritic::Smell.new(attributes) 70 | smell2 = RubyCritic::Smell.new(attributes) 71 | _(smell1).must_equal smell2 72 | end 73 | end 74 | 75 | describe '#doc_url' do 76 | it 'handles one word type names for reek smells' do 77 | smell = RubyCritic::Smell.new(type: 'Complexity', analyser: 'reek') 78 | 79 | _(smell.doc_url).must_equal('https://github.com/troessner/reek/blob/master/docs/Complexity.md') 80 | end 81 | 82 | it 'handles multiple word type names for reek smells' do 83 | smell = RubyCritic::Smell.new(type: 'TooManyStatements', analyser: 'reek') 84 | 85 | _(smell.doc_url).must_equal('https://github.com/troessner/reek/blob/master/docs/Too-Many-Statements.md') 86 | end 87 | 88 | it 'handles flay smells' do 89 | smell = RubyCritic::Smell.new(type: 'DuplicateCode', analyser: 'flay') 90 | 91 | _(smell.doc_url).must_equal('http://docs.seattlerb.org/flay/') 92 | end 93 | 94 | it 'handles flog smells' do 95 | smell = RubyCritic::Smell.new(type: 'VeryHighComplexity', analyser: 'flog') 96 | 97 | _(smell.doc_url).must_equal('http://docs.seattlerb.org/flog/') 98 | end 99 | 100 | it 'raises an error for unknown analysers' do 101 | smell = RubyCritic::Smell.new(type: 'FooSmell', analyser: 'foo') 102 | assert_raises(RuntimeError) { smell.doc_url } 103 | end 104 | end 105 | 106 | describe 'default attributes' do 107 | it 'has :new for status' do 108 | smell = RubyCritic::Smell.new 109 | _(smell.status).must_equal(:new) 110 | end 111 | 112 | it 'is has an empty array for locations' do 113 | smell = RubyCritic::Smell.new 114 | _(smell.locations).must_equal([]) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/lib/rubycritic/core/smells_array_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/core/smell' 5 | 6 | describe 'Array of Smells' do 7 | it 'is sortable' do 8 | location1 = RubyCritic::Location.new('./foo', 42) 9 | location2 = RubyCritic::Location.new('./bar', 23) 10 | location3 = RubyCritic::Location.new('./bar', 16) 11 | smell1 = RubyCritic::Smell.new(locations: [location1]) 12 | smell2 = RubyCritic::Smell.new(locations: [location2]) 13 | smell3 = RubyCritic::Smell.new(locations: [location3]) 14 | _([smell1, smell2, smell3].sort).must_equal [smell3, smell2, smell1] 15 | end 16 | 17 | it 'implements set intersection' do 18 | smell1 = RubyCritic::Smell.new(context: '#bar') 19 | smell2 = RubyCritic::Smell.new(context: '#bar') 20 | smell3 = RubyCritic::Smell.new(context: '#foo') 21 | _([smell1, smell3] & [smell2]).must_equal [smell1] 22 | end 23 | 24 | it 'implements set union' do 25 | smell1 = RubyCritic::Smell.new(context: '#bar') 26 | smell2 = RubyCritic::Smell.new(context: '#bar') 27 | smell3 = RubyCritic::Smell.new(context: '#foo') 28 | _([smell1, smell3] | [smell2]).must_equal [smell1, smell3] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/lib/rubycritic/generators/console_report_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/generators/console_report' 5 | require 'rubycritic/core/rating' 6 | require 'rubycritic/core/smell' 7 | 8 | describe RubyCritic::Generator::ConsoleReport do 9 | describe '#generate_report' do 10 | before do 11 | @mock_analysed_module = mock_analysed_module 12 | capture_output_streams do 13 | report = RubyCritic::Generator::ConsoleReport.new([@mock_analysed_module]) 14 | report.generate_report 15 | @output = $stdout.tap(&:rewind).read 16 | end 17 | end 18 | 19 | it 'outputs the report to the stdout' do 20 | refute_empty @output, 'expected report to be output to stdout' 21 | end 22 | 23 | it "starts the report with the module's name" do 24 | lines = @output.split("\n") 25 | 26 | assert_operator lines[0], :[], /#{mock_analysed_module.name}/ 27 | end 28 | 29 | it "includes the module's rating in the report" do 30 | assert output_contains?('Rating', @mock_analysed_module.rating) 31 | end 32 | 33 | it "includes the module's churn metric in the report" do 34 | assert output_contains?('Churn', @mock_analysed_module.churn) 35 | end 36 | 37 | it "includes the module's complexity in the report" do 38 | assert output_contains?('Complexity', @mock_analysed_module.complexity) 39 | end 40 | 41 | it "includes the module's duplication metric in the report" do 42 | assert output_contains?('Duplication', @mock_analysed_module.duplication) 43 | end 44 | 45 | it 'includes the number of smells in the report' do 46 | assert output_contains?('Smells', @mock_analysed_module.smells.count) 47 | end 48 | 49 | it 'includes the smell and its attributes in the report' do 50 | @mock_analysed_module.smells.each do |smell| 51 | assert output_contains?(smell), 'expected smell type and context to be reported' 52 | smell.locations.each do |location| 53 | assert output_contains?(location), 'expected all smell locations to be reported' 54 | end 55 | end 56 | end 57 | 58 | def output_contains?(*strs) 59 | @lines ||= @output.split("\n") 60 | expr = strs.map(&:to_s).map! { |s| Regexp.escape(s) }.join('.*') 61 | @lines.any? { |l| l[/#{expr}/] } 62 | end 63 | 64 | def mock_analysed_module 65 | OpenStruct.new( 66 | name: 'TestModule', 67 | rating: RubyCritic::Rating.from_cost(3), 68 | churn: 10, 69 | complexity: 0, 70 | duplication: 20, 71 | smells: [mock_smell] 72 | ) 73 | end 74 | 75 | def mock_smell 76 | smell = RubyCritic::Smell.new 77 | smell.locations << RubyCritic::Location.new(__FILE__, 3) 78 | smell.type = 'SmellySmell' 79 | smell.context = 'You' 80 | smell.message = 'Seriously, take a shower or something' 81 | smell 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/lib/rubycritic/generators/html_report_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/analysers_runner' 5 | require 'rubycritic/generators/html_report' 6 | require 'rubycritic/browser' 7 | require 'fakefs_helper' 8 | 9 | describe RubyCritic::Generator::HtmlReport do 10 | describe '#generate_report' do 11 | around do |example| 12 | capture_output_streams do 13 | with_cloned_fs(&example) 14 | end 15 | end 16 | 17 | context 'when base branch does not contain the compared file' do 18 | it 'still works' do 19 | create_analysed_modules_collection 20 | 21 | generate_report 22 | end 23 | end 24 | end 25 | 26 | def create_analysed_modules_collection 27 | RubyCritic::Config.set(root: 'test/samples') 28 | RubyCritic::Config.base_root_directory = 'test/samples' 29 | RubyCritic::Config.feature_root_directory = 'test/samples' 30 | RubyCritic::Config.compare_root_directory = 'test/samples' 31 | RubyCritic::Config.source_control_system = RubyCritic::SourceControlSystem::Git.new 32 | base_branch_collection = RubyCritic::AnalysedModulesCollection.new(['test/sample/base_branch_file.rb']) 33 | RubyCritic::Config.base_branch_collection = base_branch_collection 34 | RubyCritic::Config.mode = :compare_branches 35 | 36 | analyser_runner = RubyCritic::AnalysersRunner.new('test/samples/feature_branch_file.rb') 37 | @analysed_modules_collection = analyser_runner.run 38 | end 39 | 40 | def generate_report 41 | report = RubyCritic::Generator::HtmlReport.new(@analysed_modules_collection) 42 | report.generate_report 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/lib/rubycritic/generators/json_report_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/analysers_runner' 5 | require 'rubycritic/generators/json_report' 6 | require 'json' 7 | require 'fakefs_helper' 8 | 9 | describe RubyCritic::Generator::JsonReport do 10 | describe '#generate_report' do 11 | around do |example| 12 | capture_output_streams do 13 | with_cloned_fs(&example) 14 | end 15 | end 16 | 17 | it 'creates a report file with JSON data inside' do 18 | sample_files = Dir['test/samples/**/*.rb'] 19 | create_analysed_modules_collection 20 | generate_report 21 | data = JSON.parse(File.read('test/samples/report.json')) 22 | analysed_files = data['analysed_modules'].map { |h| h['path'] }.uniq 23 | 24 | assert_matched_arrays analysed_files, sample_files 25 | end 26 | end 27 | 28 | def create_analysed_modules_collection 29 | RubyCritic::Config.root = 'test/samples' 30 | RubyCritic::Config.source_control_system = RubyCritic::SourceControlSystem::Git.new 31 | analyser_runner = RubyCritic::AnalysersRunner.new('test/samples/') 32 | @analysed_modules_collection = analyser_runner.run 33 | end 34 | 35 | def generate_report 36 | report = RubyCritic::Generator::JsonReport.new(@analysed_modules_collection) 37 | report.generate_report 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/lib/rubycritic/generators/lint_report_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/analysers_runner' 5 | require 'rubycritic/generators/lint_report' 6 | require 'fakefs/safe' 7 | 8 | describe RubyCritic::Generator::LintReport do 9 | describe '#generate_report' do 10 | around do |example| 11 | capture_output_streams do 12 | with_cloned_fs(&example) 13 | end 14 | end 15 | 16 | it 'report file has data inside' do 17 | sample_files = Dir['test/samples/**/*.rb'].reject { |f| File.empty?(f) } 18 | create_analysed_modules_collection 19 | generate_report 20 | lines = File.readlines('test/samples/lint.txt').map(&:strip).reject(&:empty?) 21 | analysed_files = lines.map { |line| line.split(':').first }.uniq 22 | 23 | assert_matched_arrays analysed_files, sample_files 24 | end 25 | end 26 | 27 | def create_analysed_modules_collection 28 | RubyCritic::Config.root = 'test/samples' 29 | RubyCritic::Config.source_control_system = RubyCritic::SourceControlSystem::Git.new 30 | analyser_runner = RubyCritic::AnalysersRunner.new('test/samples/') 31 | @analysed_modules_collection = analyser_runner.run 32 | end 33 | 34 | def generate_report 35 | report = RubyCritic::Generator::LintReport.new(@analysed_modules_collection) 36 | report.generate_report 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/lib/rubycritic/generators/turbulence_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/generators/html/turbulence' 5 | 6 | describe RubyCritic::Turbulence do 7 | describe '::data' do 8 | it 'returns json data that maps pathname, churn and complexity to name, x and y' do 9 | files = [AnalysedModuleDouble.new(name: 'Foo', churn: 1, complexity: 2)] 10 | turbulence_data = RubyCritic::Turbulence.data(files) 11 | instance_parsed_json = JSON.parse(turbulence_data).first 12 | _(instance_parsed_json['name']).must_equal 'Foo' 13 | _(instance_parsed_json['x']).must_equal 1 14 | _(instance_parsed_json['y']).must_equal 2 15 | end 16 | end 17 | end 18 | 19 | class AnalysedModuleDouble < OpenStruct; end 20 | -------------------------------------------------------------------------------- /test/lib/rubycritic/generators/view_helpers_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/generators/html/view_helpers' 5 | require 'pathname' 6 | 7 | describe RubyCritic::ViewHelpers do 8 | context 'when the file is in the root directory' do 9 | let(:generator) { GeneratorDouble.new('foo.html') } 10 | 11 | describe '#file_path' do 12 | context 'when the other file is in the same directory' do 13 | it 'creates a relative path to a file' do 14 | _(generator.file_path('bar.html').to_s).must_equal 'bar.html' 15 | end 16 | end 17 | 18 | context 'when the other file is in a subdirectory' do 19 | it 'creates a relative path to a file' do 20 | _(generator.file_path('subdirectory/bar.html').to_s).must_equal 'subdirectory/bar.html' 21 | end 22 | end 23 | end 24 | 25 | describe '#asset_path' do 26 | it 'creates a relative path to an asset' do 27 | _(generator.asset_path('stylesheets/application.css').to_s) 28 | .must_equal 'assets/stylesheets/application.css' 29 | end 30 | end 31 | end 32 | 33 | context 'when the file is n directories deep' do 34 | let(:generator) { GeneratorDouble.new('lets/go/crazy/foo.html') } 35 | 36 | describe '#file_path' do 37 | context 'when the other file is in the same directory' do 38 | it 'creates a relative path to a file' do 39 | _(generator.file_path('lets/go/crazy/bar.html').to_s).must_equal 'bar.html' 40 | end 41 | end 42 | 43 | context 'when the other file is in the root directory' do 44 | it 'creates a relative path to a file' do 45 | _(generator.file_path('bar.html').to_s).must_equal '../../../bar.html' 46 | end 47 | end 48 | 49 | context 'when the other file has n-1 directories in common' do 50 | it 'creates a relative path to a file' do 51 | _(generator.file_path('lets/go/home/bar.html').to_s).must_equal '../home/bar.html' 52 | end 53 | end 54 | 55 | context 'when the other file is one directory deeper' do 56 | it 'creates a relative path to a file' do 57 | _(generator.file_path('lets/go/crazy/everybody/bar.html').to_s).must_equal 'everybody/bar.html' 58 | end 59 | end 60 | end 61 | 62 | describe '#asset_path' do 63 | it 'creates a relative path to an asset' do 64 | _(generator.asset_path('stylesheets/application.css').to_s) 65 | .must_equal '../../../assets/stylesheets/application.css' 66 | end 67 | end 68 | end 69 | end 70 | 71 | class GeneratorDouble 72 | include RubyCritic::ViewHelpers 73 | 74 | def initialize(file) 75 | @file = Pathname.new(file) 76 | end 77 | 78 | def file_directory 79 | root_directory + @file.dirname 80 | end 81 | 82 | def root_directory 83 | Pathname.new('root_directory') 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/lib/rubycritic/reporter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/reporter' 5 | 6 | describe RubyCritic::Reporter do 7 | before do 8 | RubyCritic::Config.set({}) 9 | RubyCritic::Config.stubs(:no_browser).returns(true) 10 | end 11 | 12 | it 'creates multiple reports' do 13 | RubyCritic::Config.stubs(:formats).returns(%i[json lint html]) 14 | create_analysed_modules_collection 15 | RubyCritic::Reporter.generate_report(@analysed_modules_collection) 16 | 17 | assert_path_exists('test/samples/report.json') 18 | assert_path_exists('test/samples/lint.txt') 19 | assert_path_exists('test/samples/overview.html') 20 | assert_path_exists('test/samples/simple_cov_index.html') 21 | end 22 | 23 | it 'creates a dummy formatter' do 24 | RubyCritic::Config.stubs(:formatters).returns(['DummyFormatter']) 25 | class DummyFormatter; end 26 | formatter = mock 27 | formatter.expects(:generate_report).returns(true) 28 | DummyFormatter.expects(:new).once.returns(formatter) 29 | create_analysed_modules_collection 30 | 31 | assert RubyCritic::Reporter.generate_report(@analysed_modules_collection) 32 | end 33 | 34 | it 'creates a dummy formatter long path' do 35 | RubyCritic::Config.stubs(:formatters).returns(['MyTest::DummyFormatter']) 36 | module MyTest 37 | class DummyFormatter; end 38 | end 39 | formatter = mock 40 | formatter.expects(:generate_report).returns(true) 41 | MyTest::DummyFormatter.expects(:new).once.returns(formatter) 42 | create_analysed_modules_collection 43 | 44 | assert RubyCritic::Reporter.generate_report(@analysed_modules_collection) 45 | end 46 | 47 | it 'creates and loads a dummy formatter' do 48 | RubyCritic::Config.stubs(:formatters).returns(['./test/samples/dummy_formatter.rb:Test::DummyFormatter']) 49 | create_analysed_modules_collection 50 | 51 | assert RubyCritic::Reporter.generate_report(@analysed_modules_collection) 52 | end 53 | 54 | def create_analysed_modules_collection 55 | RubyCritic::Config.stubs(:root).returns('./test/samples') 56 | RubyCritic::Config.stubs(:source_control_system).returns(RubyCritic::SourceControlSystem::Git.new) 57 | analyser_runner = RubyCritic::AnalysersRunner.new('test/samples/') 58 | @analysed_modules_collection = analyser_runner.run 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/lib/rubycritic/revision_comparator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/revision_comparator' 5 | 6 | describe RubyCritic::RevisionComparator do 7 | subject { RubyCritic::RevisionComparator.new([]) } 8 | 9 | describe '#statuses=' do 10 | context 'in a SCS with :revision? == false' do 11 | before do 12 | RubyCritic::Config.expects(:source_control_system) 13 | .at_least_once 14 | .returns(stub(revision?: false)) 15 | end 16 | 17 | it 'does not attempt to compare with previous results' do 18 | subject.expects(:load_cached_analysed_modules).never 19 | subject.statuses = [] 20 | end 21 | end 22 | 23 | context 'in a SCS with :revision? == true' do 24 | before do 25 | RubyCritic::Config.expects(:source_control_system) 26 | .at_least_once 27 | .returns(stub(revision?: true)) 28 | end 29 | 30 | context 'without previously cached results' do 31 | before do 32 | subject.expects(:revision_file).returns('foo') 33 | File.expects(:file?).with('foo').returns(false) 34 | end 35 | 36 | it 'does not load them' do 37 | RubyCritic::Serializer.expects(:new).never 38 | subject.statuses = [] 39 | end 40 | 41 | it 'does not invoke RubyCritic::SmellsStatusSetter' do 42 | RubyCritic::SmellsStatusSetter.expects(:set).never 43 | subject.statuses = [] 44 | end 45 | end 46 | 47 | context 'with previously cached results' do 48 | before do 49 | subject.expects(:revision_file).twice.returns('foo') 50 | File.expects(:file?).with('foo').returns(true) 51 | RubyCritic::Serializer.expects(:new).with('foo').returns(stub(load: [])) 52 | end 53 | 54 | it 'loads them' do 55 | subject.statuses = [] 56 | end 57 | 58 | it 'invokes RubyCritic::SmellsStatusSetter' do 59 | RubyCritic::SmellsStatusSetter.expects(:set).once 60 | subject.statuses = [] 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/lib/rubycritic/smells_status_setter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/core/smell' 5 | require 'rubycritic/smells_status_setter' 6 | 7 | describe RubyCritic::SmellsStatusSetter do 8 | describe '::smells' do 9 | before do 10 | @smell = RubyCritic::Smell.new(context: '#bar') 11 | @smells = [@smell] 12 | end 13 | 14 | it 'marks old smells' do 15 | RubyCritic::SmellsStatusSetter.set(@smells, @smells) 16 | _(@smell.status).must_equal :old 17 | end 18 | 19 | it 'marks new smells' do 20 | RubyCritic::SmellsStatusSetter.set([], @smells) 21 | _(@smell.status).must_equal :new 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/lib/rubycritic/source_control_systems/base_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/source_control_systems/base' 5 | 6 | describe RubyCritic::SourceControlSystem::Base do 7 | before do 8 | RubyCritic::SourceControlSystem::Base.systems.each do |system| 9 | system.stubs(:supported?).returns(false) 10 | end 11 | end 12 | 13 | describe '::create' do 14 | context 'when a source control system is found' do 15 | it 'creates an instance of that source control system' do 16 | RubyCritic::SourceControlSystem::Git.stubs(:supported?).returns(true) 17 | system = RubyCritic::SourceControlSystem::Base.create 18 | _(system).must_be_instance_of RubyCritic::SourceControlSystem::Git 19 | end 20 | end 21 | 22 | context 'when no source control system is found' do 23 | it 'creates a source control system double' do 24 | capture_output_streams do 25 | system = RubyCritic::SourceControlSystem::Base.create 26 | _(system).must_be_instance_of RubyCritic::SourceControlSystem::Double 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/lib/rubycritic/source_control_systems/double_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/source_control_systems/base' 5 | require_relative 'interfaces/basic' 6 | 7 | class DoubleTest < Minitest::Test 8 | include BasicInterface 9 | 10 | def setup 11 | @system = RubyCritic::SourceControlSystem::Double.new 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/lib/rubycritic/source_control_systems/git_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/source_control_systems/base' 5 | 6 | describe RubyCritic::SourceControlSystem::Git do 7 | describe '.switch_branch' do 8 | it 'should not raise NoMethodError' do 9 | RubyCritic::SourceControlSystem::Git.stubs(:uncommitted_changes).returns('') 10 | RubyCritic::SourceControlSystem::Git.expects(:git) 11 | RubyCritic::SourceControlSystem::Git.switch_branch('_branch_') 12 | end 13 | end 14 | 15 | describe '#churn' do 16 | let(:git) { RubyCritic::SourceControlSystem::Git.new } 17 | let(:churn_after) { 'churn_after_date' } 18 | let(:paths) { ['path/1', 'path/2'] } 19 | 20 | before do 21 | RubyCritic::SourceControlSystem::Git.stubs(:git).returns('') 22 | RubyCritic::Config.stubs(:churn_after).returns(churn_after) 23 | RubyCritic::Config.stubs(:paths).returns(paths) 24 | end 25 | 26 | it 'should pass the churn_after and path options to new Churn objects' do 27 | RubyCritic::SourceControlSystem::Git::Churn.expects(:new).with( 28 | churn_after: churn_after, paths: paths 29 | ) 30 | 31 | git.churn 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/lib/rubycritic/source_control_systems/interfaces/basic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BasicInterface 4 | def test_implements_basic_interface 5 | assert_respond_to @system, :revisions_count 6 | assert_respond_to @system, :date_of_last_commit 7 | assert_respond_to @system, :revision? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/lib/rubycritic/source_control_systems/interfaces/time_travel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This interface is only used if `@system.revision?` returns `true`. 4 | module TimeTravelInterface 5 | def test_implements_time_travel_interface 6 | assert_respond_to @system, :head_reference 7 | assert_respond_to @system, :travel_to_head 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/lib/rubycritic/source_control_systems/mercurial_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/source_control_systems/base' 5 | require_relative 'interfaces/basic' 6 | 7 | class MercurialTest < Minitest::Test 8 | include BasicInterface 9 | 10 | def setup 11 | @system = RubyCritic::SourceControlSystem::Mercurial.new 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/lib/rubycritic/source_locator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/source_locator' 5 | 6 | describe RubyCritic::SourceLocator do 7 | before do 8 | @original_dir = Dir.getwd 9 | Dir.chdir('test/samples/location') 10 | RubyCritic::Config.stubs(:ruby_extensions).returns(%w[.rb]) 11 | end 12 | 13 | describe '#paths' do 14 | it 'finds a single file' do 15 | paths = ['file0.rb'] 16 | _(RubyCritic::SourceLocator.new(paths).paths).must_equal paths 17 | end 18 | 19 | it 'finds all the files inside a given directory' do 20 | initial_paths = ['dir1'] 21 | final_paths = ['dir1/file1.rb'] 22 | _(RubyCritic::SourceLocator.new(initial_paths).paths).must_equal final_paths 23 | end 24 | 25 | it 'finds files through multiple paths' do 26 | paths = ['dir1/file1.rb', 'file0.rb'] 27 | _(RubyCritic::SourceLocator.new(paths).paths).must_match_array paths 28 | end 29 | 30 | it 'finds all the files' do 31 | initial_paths = ['.'] 32 | final_paths = ['dir1/file1.rb', 'file0.rb', 'file0_symlink.rb'] 33 | _(RubyCritic::SourceLocator.new(initial_paths).paths).must_match_array final_paths 34 | end 35 | 36 | it 'finds files with extensions it is configured to find' do 37 | RubyCritic::Config.stubs(:ruby_extensions).returns(%w[.rb .foo]) 38 | paths = ['file0.rb', 'ruby_file_different_extension.foo'] 39 | _(RubyCritic::SourceLocator.new(paths).paths).must_equal paths 40 | end 41 | 42 | it 'finds files which have a ruby shebang' do 43 | paths = ['file_with_ruby_shebang'] 44 | _(RubyCritic::SourceLocator.new(paths).paths).must_equal paths 45 | end 46 | 47 | context 'when configured to deduplicate symlinks' do 48 | it 'favors a file over a symlink if they both point to the same target' do 49 | RubyCritic::Config.stubs(:deduplicate_symlinks).returns(true) 50 | initial_paths = ['file0.rb', 'file0_symlink.rb'] 51 | final_paths = ['file0.rb'] 52 | _(RubyCritic::SourceLocator.new(initial_paths).paths).must_match_array final_paths 53 | end 54 | end 55 | 56 | it 'cleans paths of consecutive slashes and useless dots' do 57 | initial_paths = ['.//file0.rb'] 58 | final_paths = ['file0.rb'] 59 | _(RubyCritic::SourceLocator.new(initial_paths).paths).must_equal final_paths 60 | end 61 | 62 | it 'ignores paths to non-existent files' do 63 | initial_paths = ['non_existent_dir1/non_existent_file1.rb', 'non_existent_file0.rb'] 64 | final_paths = [] 65 | _(RubyCritic::SourceLocator.new(initial_paths).paths).must_equal final_paths 66 | end 67 | 68 | it 'ignores paths to files that do not match the Ruby extension' do 69 | initial_paths = ['file_with_no_extension', 'file_with_different_extension.py'] 70 | final_paths = [] 71 | _(RubyCritic::SourceLocator.new(initial_paths).paths).must_equal final_paths 72 | end 73 | 74 | it 'can deal with nil paths' do 75 | paths = nil 76 | final_paths = [] 77 | _(RubyCritic::SourceLocator.new(paths).paths).must_equal final_paths 78 | end 79 | end 80 | 81 | describe '#pathnames' do 82 | it 'finds a single file' do 83 | initial_paths = ['file0.rb'] 84 | final_pathnames = [Pathname.new('file0.rb')] 85 | _(RubyCritic::SourceLocator.new(initial_paths).pathnames).must_equal final_pathnames 86 | end 87 | end 88 | 89 | after do 90 | Dir.chdir(@original_dir) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/lib/rubycritic/version_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'rubycritic/version' 5 | 6 | describe 'RubyCritic version' do 7 | it 'is defined' do 8 | _(RubyCritic::VERSION).wont_be_nil 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/samples/base_branch_file.rb: -------------------------------------------------------------------------------- 1 | class Signup 2 | end -------------------------------------------------------------------------------- /test/samples/compare_file.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/test/samples/compare_file.rb -------------------------------------------------------------------------------- /test/samples/coverage_sample/0.18/.resultset.json: -------------------------------------------------------------------------------- 1 | { 2 | "Unit Tests": { 3 | "coverage": { 4 | "[REPLACE_ME]/lib/rubycritic/source_control_systems/double.rb": [ 5 | null, 6 | null, 7 | 1, 8 | 1, 9 | 1, 10 | 1, 11 | 0, 12 | null, 13 | null, 14 | 1, 15 | null, 16 | null, 17 | null, 18 | 1, 19 | 0, 20 | null, 21 | null, 22 | null, 23 | null 24 | ] 25 | }, 26 | "timestamp": 1570847334 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/samples/coverage_sample/0.21/.resultset.json: -------------------------------------------------------------------------------- 1 | { 2 | "Unit Tests": { 3 | "coverage": { 4 | "[REPLACE_ME]/lib/rubycritic/source_control_systems/double.rb": { 5 | "lines": [ 6 | null, 7 | null, 8 | 1, 9 | 1, 10 | 1, 11 | 1, 12 | 0, 13 | null, 14 | null, 15 | 1, 16 | null, 17 | null, 18 | null, 19 | 1, 20 | 0, 21 | null, 22 | null, 23 | null, 24 | null 25 | ] 26 | } 27 | }, 28 | "timestamp": 1615513639 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/samples/dummy_formatter.rb: -------------------------------------------------------------------------------- 1 | module Test 2 | class DummyFormatter 3 | def initialize(_analysed_modules); end 4 | def generate_report; end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/samples/empty.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/test/samples/empty.rb -------------------------------------------------------------------------------- /test/samples/feature_branch_file.rb: -------------------------------------------------------------------------------- 1 | class Signup 2 | def flay(parts) 3 | parts -= 1 4 | parts -= 2 5 | parts -= 3 6 | parts -= 4 7 | end 8 | 9 | def method_missing(method, *args, &block) 10 | message = "I" 11 | eval "message = ' did not'" 12 | eval "message << ' exist,'" 13 | eval "message << ' but now'" 14 | eval "message << ' I do.'" 15 | self.class.send(:define_method, method) { "I did not exist, but now I do." } 16 | self.send(method) 17 | end 18 | 19 | def allow_nesting_iterators_two_levels_deep 20 | loop do 21 | loop do 22 | end 23 | end 24 | end 25 | 26 | def allow_many_statements 27 | do_something 28 | do_something 29 | do_something 30 | do_something 31 | do_something 32 | do_something 33 | end 34 | end -------------------------------------------------------------------------------- /test/samples/flay/.flayignore: -------------------------------------------------------------------------------- 1 | test/samples/flay/smelly.rb 2 | -------------------------------------------------------------------------------- /test/samples/flay/smelly.rb: -------------------------------------------------------------------------------- 1 | class Ramsay 2 | def flay(parts) 3 | parts -= 1 4 | parts -= 2 5 | parts -= 3 6 | parts -= 4 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/samples/flay/smelly2.rb: -------------------------------------------------------------------------------- 1 | class Roose 2 | def flay(parts) 3 | parts -= 1 4 | parts -= 2 5 | parts -= 3 6 | parts -= 4 7 | end 8 | 9 | def duplicate(parts) 10 | parts -= 1 11 | parts -= 2 12 | parts -= 3 13 | parts -= 4 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/samples/flog/complex.rb: -------------------------------------------------------------------------------- 1 | class AllTheMethods 2 | def method_missing(method, *args, &block) 3 | message = "I" 4 | eval "message = ' did not'" 5 | eval "message << ' exist,'" 6 | eval "message << ' but now'" 7 | eval "message << ' I do.'" 8 | self.class.send(:define_method, method) { "I did not exist, but now I do." } 9 | self.send(method) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/samples/flog/smelly.rb: -------------------------------------------------------------------------------- 1 | class AllTheMethods 2 | def method_missing(method, *args, &block) 3 | message = "I" 4 | eval "message = ' did not'" 5 | eval "message << ' exist,'" 6 | eval "message << ' but now'" 7 | eval "message << ' I do.'" 8 | self.class.send(:define_method, method) { "I did not exist, but now I do." } 9 | self.send(method) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/samples/location/dir1/file1.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/test/samples/location/dir1/file1.rb -------------------------------------------------------------------------------- /test/samples/location/file0.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/test/samples/location/file0.rb -------------------------------------------------------------------------------- /test/samples/location/file0_symlink.rb: -------------------------------------------------------------------------------- 1 | file0.rb -------------------------------------------------------------------------------- /test/samples/location/file_with_different_extension.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/test/samples/location/file_with_different_extension.py -------------------------------------------------------------------------------- /test/samples/location/file_with_no_extension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/test/samples/location/file_with_no_extension -------------------------------------------------------------------------------- /test/samples/location/file_with_ruby_shebang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | -------------------------------------------------------------------------------- /test/samples/location/ruby_file_different_extension.foo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/rubycritic/65e5be47bce80627858164980e09b9707f650f47/test/samples/location/ruby_file_different_extension.foo -------------------------------------------------------------------------------- /test/samples/methods_count.rb: -------------------------------------------------------------------------------- 1 | class Example 2 | def self.one_class_method 3 | end 4 | 5 | def one_instance_method 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/samples/module_names.rb: -------------------------------------------------------------------------------- 1 | module Foo 2 | end 3 | 4 | module Foo 5 | class Bar 6 | end 7 | 8 | class Baz 9 | end 10 | end 11 | 12 | class Foo::Qux 13 | end 14 | 15 | module Foo::Quux 16 | class Corge 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/samples/no_methods.rb: -------------------------------------------------------------------------------- 1 | module Foo 2 | class NoMethods 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/samples/reek/not_smelly.rb: -------------------------------------------------------------------------------- 1 | class Perfume 2 | attr_reader :perfumed 3 | 4 | def ignoreRubyStyle(oneParameter) 5 | oneVariable = oneParameter 6 | end 7 | 8 | def allow_nesting_iterators_two_levels_deep 9 | loop do 10 | loop do 11 | end 12 | end 13 | end 14 | 15 | def allow_many_statements 16 | do_something 17 | do_something 18 | do_something 19 | do_something 20 | do_something 21 | do_something 22 | end 23 | 24 | def allow_up_to_two_duplicate_method_calls 25 | respond_to do |format| 26 | if success 27 | format.html { redirect_to some_path } 28 | format.js { head :ok } 29 | else 30 | format.html { redirect_to :back, status: :bad_request } 31 | format.js { render status: :bad_request } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/samples/reek/smelly.rb: -------------------------------------------------------------------------------- 1 | class Theon 2 | def reeks?(reek = true) 3 | reek 4 | end 5 | 6 | def flayed?(a) 7 | a 8 | end 9 | end 10 | 11 | # Reek should report 12 | # [1]:Theon has no descriptive comment (IrresponsibleModule) 13 | # [2]:Theon#reeks? has boolean parameter 'reek' (BooleanParameter) 14 | # This comment is below the module because otherwise Reek will interpret this 15 | # as a comment describing the module which would thus prevent 16 | # IrresponsibleModule from being reported. 17 | # It should ignore the UncommunicativeParameterName as it's on the .todo.reek 18 | -------------------------------------------------------------------------------- /test/samples/simple_cov_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ruby Critic - Home 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 |
24 | 25 | 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 |

Coverage

51 |
52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
RatingNameCoverage
66 |
67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV['COVERAGE'] == 'true' 4 | require 'simplecov' 5 | SimpleCov.start do 6 | track_files '/lib/' 7 | end 8 | end 9 | 10 | require 'minitest/autorun' 11 | require 'minitest/around/spec' 12 | require 'minitest/pride' 13 | require 'mocha/minitest' 14 | require 'ostruct' 15 | require 'diff/lcs' 16 | 17 | def context(...) 18 | describe(...) 19 | end 20 | 21 | def capture_output_streams 22 | $stdout = StringIO.new 23 | $stderr = StringIO.new 24 | yield 25 | ensure 26 | $stdout = STDOUT 27 | $stderr = STDERR 28 | end 29 | 30 | def with_cloned_fs 31 | FakeFS do 32 | FakeFS::FileSystem.clone(PathHelper.project_path) 33 | 34 | # reek schema is required to init reek 35 | FakeFS::FileSystem.clone(PathHelper.reek_schema_path) 36 | 37 | Dir.chdir(PathHelper.project_path) 38 | yield 39 | ensure 40 | FakeFS::FileSystem.clear 41 | end 42 | end 43 | 44 | # This class is to encapsulate avoid specs class called those paths methods accidentally 45 | module PathHelper 46 | class << self 47 | def reek_schema_path 48 | "#{Gem.loaded_specs['reek'].full_gem_path}/lib/reek/configuration/schema.yml" 49 | end 50 | 51 | def project_path 52 | File.expand_path('..', __dir__) 53 | end 54 | end 55 | end 56 | 57 | module Minitest 58 | module Assertions 59 | ## 60 | # Fails unless exp and act are both arrays and 61 | # contain the same elements. 62 | # 63 | # assert_matched_arrays [3,2,1], [1,2,3] 64 | 65 | def assert_matched_arrays(exp, act) 66 | exp_ary = exp.to_ary 67 | 68 | assert_kind_of Array, exp_ary 69 | act_ary = act.to_ary 70 | 71 | assert_kind_of Array, act_ary 72 | diffs = Diff::LCS.sdiff(act_ary.sort, exp_ary.sort).reject(&:unchanged?) 73 | 74 | assert_empty diffs, "There are diffs between expected and actual values:\n#{diffs.map(&:inspect).join("\n")}" 75 | end 76 | end 77 | 78 | module Expectations 79 | ## 80 | # See Minitest::Assertions#assert_matched_arrays 81 | # 82 | # [1,2,3].must_match_array [3,2,1] 83 | # 84 | # :method: must_match_array 85 | 86 | infect_an_assertion :assert_matched_arrays, :must_match_array 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /travis_scripts/before_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | if [ "${TRAVIS_RUBY_VERSION}" != "3.0" ]; then 4 | bundle exec appraisal install 5 | fi 6 | -------------------------------------------------------------------------------- /travis_scripts/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | echo "Using Ruby v${TRAVIS_RUBY_VERSION}" 4 | if [ "${TRAVIS_RUBY_VERSION}" = "3.0" ]; then 5 | bundle exec rake test 6 | else 7 | bundle exec appraisal rake test 8 | fi 9 | --------------------------------------------------------------------------------