rubocop-gradual 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 966ef7dacbe7b713f93a8ad8cc48af2fd39b2d76b521e4e09a5895084f4afc9d
4
+ data.tar.gz: 550a64eda37a57229e718e13c74d92d72b2e1a346f6140026ef3c13de629ee86
5
+ SHA512:
6
+ metadata.gz: 43ec3b161eb08c624f7d6014ae769dbf269c13b84d212ab50cee04b92c49eeaf66bf53d86ddec1b64cdae6d887a0284d948c56b95535a752e74414acfc32bd16
7
+ data.tar.gz: 4fdd4987b2404314f82c24c4962cc280f76815ac82bb3372c0edffe9cf18ac0ca391422efd95fcd831ffe8f48ceda2110cb9ec62f07e883032aa7bf4df01c7c3
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog],
6
+ and this project adheres to [Semantic Versioning].
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2022-07-03
11
+
12
+ ### Added
13
+
14
+ - Initial implementation. ([@skryukov])
15
+
16
+ [@skryukov]: https://github.com/skryukov
17
+
18
+ [Unreleased]: https://github.com/skryukov/rubocop-gradual/compare/v0.1.0...HEAD
19
+ [0.1.0]: https://github.com/skryukov/rubocop-gradual/commits/v0.1.0
20
+
21
+ [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
22
+ [Semantic Versioning]: https://semver.org/spec/v2.0.0.html
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Svyatoslav Kryukov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # RuboCop Gradual
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/rubocop-gradual.svg)](https://rubygems.org/gems/rubocop-gradual)
4
+ [![Build](https://github.com/skryukov/rubocop-gradual/workflows/Build/badge.svg)](https://github.com/skryukov/rubocop-gradual/actions)
5
+
6
+ RuboCop Gradual is a tool that helps track down and fix RuboCop offenses in your code gradually. It's a more flexible alternative to RuboCop's `--auto-gen-config` option.
7
+
8
+ <a href="https://evilmartians.com/?utm_source=rubocop-gradual&utm_campaign=project_page">
9
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
10
+ </a>
11
+
12
+ ## Installation
13
+
14
+ Install the gem and add to the application's Gemfile by executing:
15
+
16
+ $ bundle add rubocop-gradual
17
+
18
+ Run RuboCop Gradual to create a lock file (defaults to `.rubocop_gradual.lock`):
19
+
20
+ $ rubocop-gradual
21
+
22
+ Commit the lock file to the project repository to keep track of all non-fixed offenses.
23
+
24
+ Run `rubocop-gradual` before commiting changes to update the lock file. RuboCop Gradual will keep updating the lock file to keep track of all non-fixed offenses, but it will throw an error if there are any new offenses.
25
+
26
+ ## Usage
27
+
28
+ Proposed workflow:
29
+
30
+ - Run `rubocop-gradual` to generate a lock file and commit it to the project repository.
31
+
32
+ - Add `rubocop-gradual --CI` to your CI pipeline instead of `rubocop`/`standard`. It will throw an error if the lock file is out of date.
33
+
34
+ - Optionally, add `rubocop-gradual` as a pre-commit hook to your repository (using [lefthook], for example).
35
+
36
+ - RuboCop Gradual will throw an error on any new offense, but if you really want to force update the lock file, run `rubocop-gradual --update`.
37
+
38
+ ## Available options
39
+
40
+ ```
41
+ --ci Run Gradual in the CI mode.
42
+ -u, --update Force update Gradual lock file.
43
+ --gradual-file FILE Specify Gradual lock file.
44
+ --no-gradual Disable Gradual.
45
+ -v, --version Display version.
46
+ -V, --verbose-version Display verbose version.
47
+ -h, --help Display help message.
48
+ ```
49
+
50
+ ## Alternatives
51
+
52
+ - [RuboCop TODO file]. Comes out of the box with RuboCop. Provides a way to ignore offenses on the file level, which is problematic since it is possible to introduce new offenses without any signal from linter.
53
+ - [Pronto]. Checks for offenses only on changed files. Does not provide a way to temporarily ignore offenses.
54
+ - [Betterer]. Universal test runner that helps make incremental improvements witten in JavaScript. RuboCop Gradual is highly inspired by Betterer.
55
+
56
+ ## Development
57
+
58
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
59
+
60
+ To install this gem onto your local machine, run `bundle exec rake install`.
61
+
62
+ ## Contributing
63
+
64
+ Bug reports and pull requests are welcome on GitHub at https://github.com/skryukov/rubocop-gradual
65
+
66
+ ## License
67
+
68
+ The gem is available as open source under the terms of the [MIT License].
69
+
70
+ [lefthook]: https://github.com/evilmartians/lefthook
71
+ [RuboCop TODO file]: https://docs.rubocop.org/rubocop/configuration.html#automatically-generated-configuration
72
+ [Pronto]: https://github.com/prontolabs/pronto-rubocop
73
+ [Betterer]: https://github.com/phenomnomnominal/betterer
74
+ [MIT License]: https://opensource.org/licenses/MIT
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
+
6
+ require "rubocop-gradual"
7
+ require "benchmark"
8
+
9
+ exit_status = 0
10
+ cli = RuboCop::Gradual::CLI.new
11
+
12
+ time = Benchmark.realtime { exit_status = cli.run }
13
+
14
+ puts "Finished in #{time} seconds" if cli.options[:debug] || cli.options[:display_time]
15
+
16
+ exit exit_status
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+ require_relative "formatter"
5
+
6
+ module RuboCop
7
+ module Gradual
8
+ # CLI is a wrapper around RuboCop::CLI.
9
+ class CLI < RuboCop::CLI
10
+ def run(args = ARGV)
11
+ Gradual.mode = :base
12
+ rubocop_args = Options.new.parse(args)
13
+ super(rubocop_args)
14
+ end
15
+
16
+ private
17
+
18
+ def apply_default_formatter
19
+ return super if Gradual.mode == :disabled
20
+ raise OptionArgumentError, "-f, --format cannot be used in gradual mode." if @options[:formatters]
21
+
22
+ @options[:formatters] = [[Formatter, nil]]
23
+ end
24
+
25
+ def execute_runners
26
+ raise OptionArgumentError, "--auto-gen-config cannot be used in gradual mode." if @options[:auto_gen_config]
27
+
28
+ result = super
29
+ Gradual.mode == :disabled ? result : Gradual.exit_code
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+ require "pathname"
5
+
6
+ require_relative "process"
7
+
8
+ module RuboCop
9
+ module Gradual
10
+ # Formatter is a RuboCop formatter class that collects RuboCop results and
11
+ # calls the Gradual::Process class at the end to process them.
12
+ class Formatter < RuboCop::Formatter::BaseFormatter
13
+ include PathUtil
14
+
15
+ attr_reader :output_hash
16
+
17
+ def initialize(_output, options = {})
18
+ super
19
+ Gradual.debug = options[:debug]
20
+ puts "Gradual mode: #{Gradual.mode}" if Gradual.debug
21
+ @output_hash = { files: [] }
22
+ end
23
+
24
+ def file_finished(file, offenses)
25
+ print "."
26
+ return if offenses.empty?
27
+
28
+ output_hash[:files] << {
29
+ path: smart_path(file),
30
+ issues: offenses.reject(&:corrected?).map { |o| issue_offense(o) }
31
+ }
32
+ end
33
+
34
+ def finished(_inspected_files)
35
+ puts "\n#{stats_message}"
36
+ puts "Processing results..."
37
+
38
+ time = Benchmark.realtime { Process.new(output_hash).call }
39
+
40
+ puts "Finished Gradual processing in #{time} seconds" if options[:debug] || options[:display_time]
41
+ end
42
+
43
+ private
44
+
45
+ def issue_offense(offense)
46
+ {
47
+ line: offense.line,
48
+ column: offense.real_column,
49
+ length: offense.location.length,
50
+ message: offense.message
51
+ }
52
+ end
53
+
54
+ def stats_message
55
+ issues_count = output_hash[:files].sum { |f| f[:issues].size }
56
+ "Found #{output_hash[:files].size} files with #{issues_count} issue(s)."
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "diffy"
4
+
5
+ require_relative "serializer"
6
+
7
+ module RuboCop
8
+ module Gradual
9
+ # LockFile class handles reading and writing of lock file.
10
+ class LockFile
11
+ attr_reader :path
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ end
16
+
17
+ def read_results
18
+ return unless File.exist?(path)
19
+
20
+ Serializer.deserialize(content)
21
+ end
22
+
23
+ def delete
24
+ return unless File.exist?(path)
25
+
26
+ File.delete(path)
27
+ end
28
+
29
+ def write_results(results)
30
+ File.write(path, Serializer.serialize(results), encoding: Encoding::UTF_8)
31
+ end
32
+
33
+ def diff(new_results)
34
+ Diffy::Diff.new(Serializer.serialize(new_results), content, context: 0)
35
+ end
36
+
37
+ private
38
+
39
+ def content
40
+ @content ||= File.exist?(path) ? File.read(path, encoding: Encoding::UTF_8) : ""
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rainbow"
4
+ require "shellwords"
5
+
6
+ module RuboCop
7
+ module Gradual
8
+ # Options class defines RuboCop Gradual cli options.
9
+ # It also extracts command line RuboCop Gradual arguments
10
+ # before passing leftover arguments to RuboCop::CLI.
11
+ class Options
12
+ def parse(args)
13
+ parser = define_options
14
+ @gradual_args, @rubocop_args = filter_args(parser, args_from_file + args)
15
+ parser.parse(@gradual_args)
16
+ @rubocop_args
17
+ end
18
+
19
+ private
20
+
21
+ def define_options
22
+ OptionParser.new do |opts|
23
+ opts.banner = rainbow.wrap("\nGradual options:").bright
24
+
25
+ define_gradual_options(opts)
26
+
27
+ define_proxy_options(opts)
28
+ end
29
+ end
30
+
31
+ def define_gradual_options(opts)
32
+ opts.on("-u", "--update", "Force update Gradual lock file.") { Gradual.mode = :update }
33
+
34
+ opts.on("--ci", "Run Gradual in the CI mode.") { Gradual.mode = :ci }
35
+
36
+ opts.on("--gradual-file FILE", "Specify Gradual lock file.") { |path| Gradual.path = path }
37
+
38
+ opts.on("--no-gradual", "Disable Gradual.") { Gradual.mode = :disabled }
39
+ end
40
+
41
+ def define_proxy_options(opts)
42
+ proxy_option(opts, "-v", "--version", "Display version.") do
43
+ print "rubocop-gradual: #{VERSION}\nrubocop: "
44
+ end
45
+
46
+ proxy_option(opts, "-V", "--verbose-version", "Display verbose version.") do
47
+ print "rubocop-gradual: #{VERSION}\nrubocop:"
48
+ end
49
+
50
+ proxy_option(opts, "-h", "--help", "Display help message.") do
51
+ at_exit { puts opts }
52
+ end
53
+ end
54
+
55
+ def proxy_option(opts, *attrs)
56
+ opts.on(*attrs) do
57
+ @rubocop_args << attrs[0]
58
+ yield
59
+ end
60
+ end
61
+
62
+ def filter_args(parser, original_args, self_args = [])
63
+ extract_all_args(parser).each do |arg|
64
+ loop do
65
+ break unless (i = original_args.index { |a| a.start_with?(arg) })
66
+
67
+ loop do
68
+ self_args << original_args.delete_at(i)
69
+ break if original_args.size <= i || original_args[i].start_with?("-")
70
+ end
71
+ end
72
+ end
73
+ [self_args, original_args]
74
+ end
75
+
76
+ def extract_all_args(parser)
77
+ parser.top.list.reduce([]) do |res, option|
78
+ res + option.long + option.short
79
+ end
80
+ end
81
+
82
+ def args_from_file
83
+ if File.exist?(".rubocop-gradual") && !File.directory?(".rubocop-gradual")
84
+ File.read(".rubocop-gradual").shellsplit
85
+ else
86
+ []
87
+ end
88
+ end
89
+
90
+ def rainbow
91
+ @rainbow ||= Rainbow.new.tap do |r|
92
+ r.enabled = false if ARGV.include?("--no-color")
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "diff"
4
+ require_relative "matcher"
5
+
6
+ module RuboCop
7
+ module Gradual
8
+ class Process
9
+ # CalculateDiff calculates the difference between two RuboCop Gradual results.
10
+ module CalculateDiff
11
+ class << self
12
+ def call(new_result, old_result)
13
+ return Diff.new.add_new(new_result.files) if old_result.nil?
14
+
15
+ diff_results(new_result, old_result)
16
+ end
17
+
18
+ private
19
+
20
+ def diff_results(new_result, old_result)
21
+ new_files, fixed_files, path_files_match, moved_files_match = split_files(new_result, old_result)
22
+
23
+ diff = Diff.new.add_new(new_files).add_fixed(fixed_files)
24
+ path_files_match.chain(moved_files_match).each do |result_file, old_file|
25
+ diff_issues(diff, result_file, old_file)
26
+ end
27
+
28
+ diff
29
+ end
30
+
31
+ def split_files(new_result, old_result)
32
+ path_files_match = Matcher.new(new_result.files, old_result.files, :path)
33
+ new_or_moved_files = path_files_match.unmatched_keys
34
+ fixed_or_moved_files = path_files_match.unmatched_values
35
+
36
+ moved_files_match = Matcher.new(new_or_moved_files, fixed_or_moved_files, :file_hash)
37
+ new_files = moved_files_match.unmatched_keys
38
+ fixed_files = moved_files_match.unmatched_values
39
+
40
+ [new_files, fixed_files, path_files_match, moved_files_match]
41
+ end
42
+
43
+ def diff_issues(diff, result_file, old_file)
44
+ fixed_or_moved_issues = old_file.changed_issues(result_file)
45
+ new_or_moved_issues = result_file.changed_issues(old_file)
46
+ moved_issues, fixed_issues = split_issues(fixed_or_moved_issues, new_or_moved_issues)
47
+
48
+ diff.add_issues(
49
+ result_file.path,
50
+ fixed: fixed_issues,
51
+ moved: moved_issues,
52
+ new: new_or_moved_issues - moved_issues,
53
+ unchanged: result_file.issues - new_or_moved_issues - moved_issues
54
+ )
55
+ end
56
+
57
+ def split_issues(fixed_or_moved_issues, new_or_moved_issues)
58
+ possibilities = new_or_moved_issues.dup
59
+ fixed_issues = []
60
+ moved_issues = []
61
+ fixed_or_moved_issues.each do |fixed_or_moved_issue|
62
+ best = best_possibility(fixed_or_moved_issue, possibilities)
63
+ next fixed_issues << fixed_or_moved_issue if best.nil?
64
+
65
+ moved_issues << possibilities.delete(best)
66
+ end
67
+ [moved_issues, fixed_issues]
68
+ end
69
+
70
+ def best_possibility(issue, possible_issues)
71
+ possibilities = possible_issues.select do |possible_issue|
72
+ possible_issue.code_hash == issue.code_hash
73
+ end
74
+ possibilities.min_by { |possibility| issue.distance(possibility) }
75
+ end
76
+
77
+ def map_same_files(left, right)
78
+ map_files(left.files, right.files) do |new_file, old_file|
79
+ new_file.path == old_file.path
80
+ end
81
+ end
82
+
83
+ def map_files(key_files, value_files)
84
+ key_files.each_with_object({}) do |key_file, res|
85
+ same_file = value_files.find { |value_file| yield(key_file, value_file) }
86
+ res[key_file] = same_file if same_file
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Gradual
5
+ class Process
6
+ # Diff class represents the difference between two RuboCop Gradual results.
7
+ class Diff
8
+ attr_reader :files
9
+
10
+ def initialize
11
+ @files = {}
12
+ end
13
+
14
+ def state
15
+ return :new if new?
16
+ return :complete if statistics[:left].zero?
17
+ return :worse if statistics[:new].positive?
18
+ return :better if statistics[:fixed].positive?
19
+ return :updated if statistics[:moved].positive?
20
+
21
+ :no_changes
22
+ end
23
+
24
+ def statistics
25
+ @statistics ||=
26
+ begin
27
+ fixed = count_issues(:fixed)
28
+ moved = count_issues(:moved)
29
+ new = count_issues(:new)
30
+ unchanged = count_issues(:unchanged)
31
+ left = moved + new + unchanged
32
+ { fixed: fixed, moved: moved, new: new, unchanged: unchanged, left: left }
33
+ end
34
+ end
35
+
36
+ def add_new(files)
37
+ files.each do |file|
38
+ add_issues(file.path, new: file.issues)
39
+ end
40
+ self
41
+ end
42
+
43
+ def add_fixed(files)
44
+ files.each do |file|
45
+ add_issues(file.path, fixed: file.issues)
46
+ end
47
+ self
48
+ end
49
+
50
+ def add_issues(path, fixed: [], moved: [], new: [], unchanged: [])
51
+ @files[path] = {
52
+ fixed: fixed,
53
+ moved: moved,
54
+ new: new,
55
+ unchanged: unchanged
56
+ }
57
+ log_file_issues(path) if RuboCop::Gradual.debug
58
+ self
59
+ end
60
+
61
+ private
62
+
63
+ def new?
64
+ statistics[:new].positive? && statistics[:new] == statistics[:left]
65
+ end
66
+
67
+ def count_issues(key)
68
+ @files.values.sum { |v| v[key].size }
69
+ end
70
+
71
+ def log_file_issues(file_path)
72
+ puts "#{file_path}:"
73
+ @files[file_path].each do |key, issues|
74
+ puts " #{key}: #{issues.size}"
75
+ next if issues.empty?
76
+
77
+ puts " #{issues.join("\n ")}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Gradual
5
+ class Process
6
+ # Matcher matches two files arrays and returns an object with matched map.
7
+ class Matcher
8
+ include Enumerable
9
+
10
+ attr_reader :unmatched_keys, :unmatched_values, :matched
11
+
12
+ def initialize(keys, values, matched)
13
+ @unmatched_keys = keys - matched.keys
14
+ @unmatched_values = values - matched.values
15
+ @matched = matched
16
+ end
17
+
18
+ def each(&block)
19
+ @matched.each(&block)
20
+ end
21
+
22
+ class << self
23
+ def new(keys, values, property)
24
+ matched = keys.each_with_object({}) do |key, result|
25
+ match_value = values.find do |value|
26
+ key.public_send(property) == value.public_send(property)
27
+ end
28
+ result[key] = match_value if match_value
29
+ end
30
+
31
+ super(keys, values, matched)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Gradual
5
+ class Process
6
+ # Printer class prints the results of the RuboCop Gradual process.
7
+ class Printer
8
+ def initialize(diff)
9
+ @diff = diff
10
+ end
11
+
12
+ def print_results
13
+ puts diff.statistics if RuboCop::Gradual.debug
14
+
15
+ send "print_#{diff.state}"
16
+ end
17
+
18
+ def print_ci_warning(diff)
19
+ puts <<~MSG
20
+ \n#{bold("Unexpected Changes!")}
21
+
22
+ RuboCop Gradual lock file is outdated, to fix this message:
23
+ - Run `rubocop-gradual` locally and commit the results, or
24
+ - EVEN BETTER: before doing the above, try to fix the remaining issues in those files!
25
+
26
+ #{bold("`#{Gradual.path}` diff:")}
27
+
28
+ #{diff.to_s(ARGV.include?("--no-color") ? :text : :color)}
29
+ MSG
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :diff
35
+
36
+ def print_complete
37
+ puts bold("RuboCop Gradual is complete!")
38
+ puts "Removing `#{Gradual.path}` lock file..."
39
+ end
40
+
41
+ def print_updated
42
+ puts bold("RuboCop Gradual got its results updated.")
43
+ end
44
+
45
+ def print_no_changes
46
+ puts bold("RuboCop Gradual got no changes.")
47
+ end
48
+
49
+ def print_new
50
+ issues_left = diff.statistics[:left]
51
+ puts bold("RuboCop Gradual got results for the first time. #{issues_left} issue(s) found.")
52
+ puts "Don't forget to commit `#{Gradual.path}` log file."
53
+ end
54
+
55
+ def print_better
56
+ issues_left = diff.statistics[:left]
57
+ issues_fixed = diff.statistics[:fixed]
58
+ puts bold("RuboCop Gradual got #{issues_fixed} issue(s) fixed, #{issues_left} left. Keep going!")
59
+ end
60
+
61
+ def print_worse
62
+ puts bold("Uh oh, RuboCop Gradual got worse:")
63
+ print_new_issues
64
+ puts bold("Force updating lock file...") if Gradual.mode == :update
65
+ end
66
+
67
+ def print_new_issues
68
+ diff.files.each do |path, issues|
69
+ next if issues[:new].empty?
70
+
71
+ puts "-> #{path} (#{issues[:new].size} new issues)"
72
+ issues[:new].each do |issue|
73
+ puts " (line #{issue.line}) \"#{issue.message}\""
74
+ end
75
+ end
76
+ end
77
+
78
+ def bold(str)
79
+ rainbow.wrap(str).bright
80
+ end
81
+
82
+ def rainbow
83
+ @rainbow ||= Rainbow.new.tap do |r|
84
+ r.enabled = false if ARGV.include?("--no-color")
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "results"
4
+ require_relative "lock_file"
5
+ require_relative "process/calculate_diff"
6
+ require_relative "process/printer"
7
+
8
+ module RuboCop
9
+ module Gradual
10
+ # Process is a class that handles the processing of RuboCop results.
11
+ class Process
12
+ attr_reader :new_results, :lock_file
13
+
14
+ def initialize(rubocop_result)
15
+ @lock_file = LockFile.new(Gradual.path)
16
+ @new_results = Results.new(**rubocop_result)
17
+ end
18
+
19
+ def call
20
+ diff = CalculateDiff.call(new_results, lock_file.read_results)
21
+ printer = Printer.new(diff)
22
+ if print_ci_warning?(diff)
23
+ printer.print_ci_warning(lock_file.diff(new_results))
24
+ else
25
+ printer.print_results
26
+ end
27
+
28
+ Gradual.exit_code = error_code(diff)
29
+
30
+ sync_lock_file(diff)
31
+ end
32
+
33
+ private
34
+
35
+ def print_ci_warning?(diff)
36
+ Gradual.mode == :ci && diff.state != :no_changes && diff.state != :worse
37
+ end
38
+
39
+ def sync_lock_file(diff)
40
+ return unless Gradual.exit_code.zero?
41
+ return lock_file.delete if diff.state == :complete
42
+
43
+ lock_file.write_results(new_results)
44
+ end
45
+
46
+ def error_code(diff)
47
+ return 1 if print_ci_warning?(diff)
48
+ return 1 if diff.state == :worse && Gradual.mode != :update
49
+
50
+ 0
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "issue"
4
+
5
+ module RuboCop
6
+ module Gradual
7
+ class Results
8
+ # File is a representation of a file in a Gradual results.
9
+ class File
10
+ attr_reader :path, :issues, :file_hash, :state
11
+
12
+ def initialize(path:, issues:, hash: nil)
13
+ @path = path
14
+ @file_hash = hash || djb2a(data)
15
+ @issues = prepare_issues(issues).sort
16
+ @data = nil
17
+ end
18
+
19
+ def <=>(other)
20
+ path <=> other.path
21
+ end
22
+
23
+ def changed_issues(other_file)
24
+ issues.reject do |result_issue|
25
+ other_file.issues.find { |other_issue| result_issue == other_issue }
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def prepare_issues(issues)
32
+ issues.map { |issue| Issue.new(**issue.merge(hash: issue_hash(issue))) }
33
+ end
34
+
35
+ def issue_hash(issue)
36
+ return issue[:hash] if issue[:hash]
37
+
38
+ code = data.lines[issue[:line] - 1..].join("\n")[issue[:column] - 1, issue[:length]]
39
+ djb2a(code)
40
+ end
41
+
42
+ def data
43
+ @data ||= ::File.read(path, encoding: Encoding::UTF_8)
44
+ end
45
+
46
+ # Function used to calculate the version hash for files and code parts.
47
+ # @see http://www.cse.yorku.ca/~oz/hash.html#djb2
48
+ def djb2a(str)
49
+ str.each_byte.inject(5381) do |hash, b|
50
+ ((hash << 5) + hash) ^ b
51
+ end & 0xFFFFFFFF
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Gradual
5
+ class Results
6
+ # IssueResults is a representation of an issue in a Gradual results.
7
+ class Issue
8
+ attr_reader :line, :column, :length, :message, :code_hash
9
+
10
+ def initialize(line:, column:, length:, message:, hash:)
11
+ @line = line
12
+ @column = column
13
+ @length = length
14
+ @message = message
15
+ @code_hash = hash
16
+ end
17
+
18
+ def <=>(other)
19
+ [line, column, length] <=> [other.line, other.column, other.length]
20
+ end
21
+
22
+ def to_s
23
+ "[#{[line, column, length, message.to_json, code_hash].join(", ")}]"
24
+ end
25
+
26
+ def ==(other)
27
+ line == other.line && column == other.column && length == other.length && code_hash == other.code_hash
28
+ end
29
+
30
+ def distance(other)
31
+ [(line - other.line).abs, (column - other.column).abs]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "results/file"
4
+
5
+ module RuboCop
6
+ module Gradual
7
+ # Results is a collection of FileResults.
8
+ class Results
9
+ attr_reader :files
10
+
11
+ def initialize(files:)
12
+ @files = files.map { |file| File.new(**file) }.sort
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Gradual
5
+ # Serializer is a module used to serialize and deserialize RuboCop results to the lock file.
6
+ module Serializer
7
+ class << self
8
+ def serialize(results)
9
+ "#{serialize_files(results.files)}\n"
10
+ end
11
+
12
+ def deserialize(data)
13
+ files = JSON.parse(data).map do |key, value|
14
+ path, hash = key.split(":")
15
+ raise Error, "Wrong format of the lock file: `#{key}` must include hash" if hash.nil? || hash.empty?
16
+
17
+ issues = value.map do |line, column, length, message, issue_hash|
18
+ { line: line, column: column, length: length, message: message, hash: issue_hash }
19
+ end
20
+ { path: path, hash: hash.to_i, issues: issues }
21
+ end
22
+ Results.new(files: files)
23
+ end
24
+
25
+ private
26
+
27
+ def serialize_files(files)
28
+ data = files.map do |file|
29
+ key = "#{file.path}:#{file.file_hash}"
30
+ issues = serialize_issues(file.issues)
31
+ [key, issues]
32
+ end
33
+ key_values_to_json(data)
34
+ end
35
+
36
+ def serialize_issues(issues)
37
+ "[\n#{indent(issues.join(",\n"))}\n]"
38
+ end
39
+
40
+ def key_values_to_json(arr)
41
+ arr.map { |key, value| indent(%("#{key}": #{value})) }
42
+ .join(",\n")
43
+ .then { |data| "{\n#{data}\n}" }
44
+ end
45
+
46
+ def indent(str, indent_str = " ")
47
+ str.lines
48
+ .map { |line| indent_str + line }
49
+ .join
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Gradual
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ require_relative "gradual/version"
6
+ require_relative "gradual/cli"
7
+
8
+ module RuboCop
9
+ # RuboCop Gradual project namespace
10
+ module Gradual
11
+ class Error < StandardError; end
12
+
13
+ class << self
14
+ attr_accessor :debug, :exit_code, :mode, :path
15
+
16
+ def set_defaults!
17
+ self.debug = false
18
+ self.exit_code = 0
19
+ self.mode = :base
20
+ self.path = ".rubocop_gradual.lock"
21
+ end
22
+ end
23
+
24
+ set_defaults!
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubocop/gradual"
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-gradual
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Svyatoslav Kryukov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-07-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: diff-lcs
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.2.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: diffy
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rainbow
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.2.2
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '4.0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.2.2
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '4.0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rubocop
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.0'
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.0'
81
+ description: Gradually improve your code with RuboCop.
82
+ email:
83
+ - s.g.kryukov@yandex.ru
84
+ executables:
85
+ - rubocop-gradual
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - CHANGELOG.md
90
+ - LICENSE.txt
91
+ - README.md
92
+ - exe/rubocop-gradual
93
+ - lib/rubocop-gradual.rb
94
+ - lib/rubocop/gradual.rb
95
+ - lib/rubocop/gradual/cli.rb
96
+ - lib/rubocop/gradual/formatter.rb
97
+ - lib/rubocop/gradual/lock_file.rb
98
+ - lib/rubocop/gradual/options.rb
99
+ - lib/rubocop/gradual/process.rb
100
+ - lib/rubocop/gradual/process/calculate_diff.rb
101
+ - lib/rubocop/gradual/process/diff.rb
102
+ - lib/rubocop/gradual/process/matcher.rb
103
+ - lib/rubocop/gradual/process/printer.rb
104
+ - lib/rubocop/gradual/results.rb
105
+ - lib/rubocop/gradual/results/file.rb
106
+ - lib/rubocop/gradual/results/issue.rb
107
+ - lib/rubocop/gradual/serializer.rb
108
+ - lib/rubocop/gradual/version.rb
109
+ homepage: https://github.com/skryukov/paco
110
+ licenses:
111
+ - MIT
112
+ metadata:
113
+ bug_tracker_uri: https://github.com/skryukov/paco/issues
114
+ changelog_uri: https://github.com/skryukov/paco/blob/main/CHANGELOG.md
115
+ documentation_uri: https://github.com/skryukov/paco/blob/main/README.md
116
+ homepage_uri: https://github.com/skryukov/paco
117
+ source_code_uri: https://github.com/skryukov/paco
118
+ rubygems_mfa_required: 'true'
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 2.6.0
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.2.15
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Gradual RuboCop plugin
138
+ test_files: []