rubocop-gradual 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/exe/rubocop-gradual +16 -0
- data/lib/rubocop/gradual/cli.rb +33 -0
- data/lib/rubocop/gradual/formatter.rb +60 -0
- data/lib/rubocop/gradual/lock_file.rb +44 -0
- data/lib/rubocop/gradual/options.rb +97 -0
- data/lib/rubocop/gradual/process/calculate_diff.rb +93 -0
- data/lib/rubocop/gradual/process/diff.rb +83 -0
- data/lib/rubocop/gradual/process/matcher.rb +37 -0
- data/lib/rubocop/gradual/process/printer.rb +90 -0
- data/lib/rubocop/gradual/process.rb +54 -0
- data/lib/rubocop/gradual/results/file.rb +56 -0
- data/lib/rubocop/gradual/results/issue.rb +36 -0
- data/lib/rubocop/gradual/results.rb +16 -0
- data/lib/rubocop/gradual/serializer.rb +54 -0
- data/lib/rubocop/gradual/version.rb +7 -0
- data/lib/rubocop/gradual.rb +26 -0
- data/lib/rubocop-gradual.rb +3 -0
- metadata +138 -0
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
|
+
[](https://rubygems.org/gems/rubocop-gradual)
|
4
|
+
[](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
|
data/exe/rubocop-gradual
ADDED
@@ -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,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
|
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: []
|