rubocop_director 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/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.standard.yml +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +79 -0
- data/Rakefile +10 -0
- data/exe/rubocop-director +5 -0
- data/lib/rubocop_director/commands/generate_config.rb +37 -0
- data/lib/rubocop_director/commands/plan.rb +54 -0
- data/lib/rubocop_director/file_stats_builder.rb +65 -0
- data/lib/rubocop_director/git_log_stats.rb +28 -0
- data/lib/rubocop_director/output_formatter.rb +32 -0
- data/lib/rubocop_director/rubocop_stats.rb +28 -0
- data/lib/rubocop_director/runner.rb +39 -0
- data/lib/rubocop_director/version.rb +5 -0
- data/lib/rubocop_director.rb +8 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9495c42ab7429abd8ad0cf4c31074079fba3419b9cccc56a9ab51fdd66ae3c49
|
4
|
+
data.tar.gz: e634bc7e5f50acedb08be3c5dbb2f2e8ea03cbda5ef9694f89e87c5943b16a8d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5ad6d7874bae6bc6bab004da66c96b777a24bec8f50457a7dc46aabb7b5e349bc49d52d2866e4f06c14c2ad71e9840d03c9fcc088d1d237061f80acfef3735f8
|
7
|
+
data.tar.gz: 8b1aeda0559907b4696f47b77a069f605816e7d25b99318bb36e54288511c859350d144ddc8469c40e8c347afb0c2c16cbd0cd956d02200974703b9decbb04f4
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.standard.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby_version: 3.2
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 DmitryTsepelev
|
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,79 @@
|
|
1
|
+
# RubocopDirector
|
2
|
+
|
3
|
+
A command–line utility for refactoring planning. It uses `.rubocop_todo.yml` and git history to prioritize a list of refactorings that can bring the most value.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Prerequisites:
|
8
|
+
|
9
|
+
- `sed`;
|
10
|
+
- `git` repo;
|
11
|
+
- generated `.rubocop_todo.yml`.
|
12
|
+
|
13
|
+
Install the gem and add to the application's Gemfile by executing:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle add rubocop_director
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
First of all, create the initial config file based on `.rubocop_todo.yml`:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
bundle exec rubocop-director --generate-config
|
25
|
+
```
|
26
|
+
|
27
|
+
Optionally adjust weights in the config:
|
28
|
+
|
29
|
+
1. `update_weight` means how important the fact that file was recently updated;
|
30
|
+
2. `default_cop_weight` will be used in case when weight for a specific cop is not set;
|
31
|
+
3. `weights` contains weights for specific cops.
|
32
|
+
|
33
|
+
_You can use any numbers you want, but I think it's better to stick with something from 0 to 1._
|
34
|
+
|
35
|
+
Build the report:
|
36
|
+
|
37
|
+
```bash
|
38
|
+
bundle exec rubocop-director
|
39
|
+
```
|
40
|
+
|
41
|
+
As a result you'll get something like this:
|
42
|
+
|
43
|
+
```bash
|
44
|
+
đź’ˇ Checking git history since 1995-01-01 to find hot files...
|
45
|
+
💡🎥 Running rubocop to get the list of offences to fix...
|
46
|
+
💡🎥🎬 Calculating a list of files to refactor...
|
47
|
+
--------------------
|
48
|
+
spec/models/user.rb
|
49
|
+
updated 10 times since -4712-01-01
|
50
|
+
offences: RSpec/AroundBlock - 8
|
51
|
+
refactoring value: 110 (55%)
|
52
|
+
--------------------
|
53
|
+
spec/models/order.rb
|
54
|
+
updated 20 times since -4712-01-01
|
55
|
+
offences: Rspec/BeEql - 4
|
56
|
+
refactoring value: 90 (45%)
|
57
|
+
```
|
58
|
+
|
59
|
+
> Want a different output format (e.g., CSV)? Let me know, open an issue!
|
60
|
+
|
61
|
+
Value is calculated using a formula: `sum of value from each cop (<number of offences> * <cop weight> * <number of file updates> * <update weight>)`.
|
62
|
+
|
63
|
+
If you need to count updates from a specific date—use `--since`:
|
64
|
+
|
65
|
+
```bash
|
66
|
+
bundle exec rubocop-director --since=2023-01-01
|
67
|
+
```
|
68
|
+
|
69
|
+
## Development
|
70
|
+
|
71
|
+
After checking out the repo, run `bundle insatll` to install dependencies
|
72
|
+
|
73
|
+
## Contributing
|
74
|
+
|
75
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/DmitryTsepelev/rubocop_director.
|
76
|
+
|
77
|
+
## License
|
78
|
+
|
79
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "dry/monads"
|
2
|
+
|
3
|
+
module RubocopDirector
|
4
|
+
module Commands
|
5
|
+
class GenerateConfig
|
6
|
+
include Dry::Monads[:result]
|
7
|
+
include Dry::Monads::Do.for(:run)
|
8
|
+
|
9
|
+
RUBOCOP_TODO = ".rubocop_todo.yml"
|
10
|
+
|
11
|
+
def run
|
12
|
+
todo = yield load_config
|
13
|
+
|
14
|
+
weights = todo.keys.each_with_object({}).each do |cop, acc|
|
15
|
+
acc.merge!(cop => 1)
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO: warn if file exists
|
19
|
+
File.write(".rubocop-director.yml", {
|
20
|
+
"update_weight" => 1,
|
21
|
+
"default_cop_weight" => 1,
|
22
|
+
"weights" => weights
|
23
|
+
}.to_yaml)
|
24
|
+
|
25
|
+
Success("Config generated")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def load_config
|
31
|
+
Success(YAML.load_file(RUBOCOP_TODO))
|
32
|
+
rescue Errno::ENOENT
|
33
|
+
Failure("#{RUBOCOP_TODO} not found, generate it using `rubocop --regenerate-todo`")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "open3"
|
2
|
+
require "json"
|
3
|
+
require "yaml"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
require "rubocop_director/rubocop_stats"
|
7
|
+
require "rubocop_director/git_log_stats"
|
8
|
+
require "rubocop_director/file_stats_builder"
|
9
|
+
require "rubocop_director/output_formatter"
|
10
|
+
|
11
|
+
module RubocopDirector
|
12
|
+
module Commands
|
13
|
+
class Plan
|
14
|
+
include Dry::Monads[:result]
|
15
|
+
include Dry::Monads::Do.for(:run)
|
16
|
+
|
17
|
+
def initialize(since)
|
18
|
+
@since = since || "1995-01-01"
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
config = yield load_config
|
23
|
+
update_counts = yield load_git_stats
|
24
|
+
rubocop_json = yield load_rubocop_json
|
25
|
+
ranged_files = yield range_files(rubocop_json: rubocop_json, update_counts: update_counts, config: config)
|
26
|
+
|
27
|
+
OutputFormatter.new(ranged_files: ranged_files, since: @since).call
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def load_config
|
33
|
+
Success(YAML.load_file(CONFIG_NAME))
|
34
|
+
rescue Errno::ENOENT
|
35
|
+
Failure("#{CONFIG_NAME} not found, generate it using `rubocop-director --generate-config`")
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_git_stats
|
39
|
+
puts "đź’ˇ Checking git history since #{@since} to find hot files..."
|
40
|
+
GitLogStats.new(@since).fetch
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_rubocop_json
|
44
|
+
puts "💡🎥 Running rubocop to get the list of offences to fix..."
|
45
|
+
RubocopStats.new.fetch
|
46
|
+
end
|
47
|
+
|
48
|
+
def range_files(rubocop_json:, update_counts:, config:)
|
49
|
+
puts "💡🎥🎬 Calculating a list of files to refactor..."
|
50
|
+
RubocopDirector::FileStatsBuilder.new(rubocop_json: rubocop_json, update_counts: update_counts, config: config).build
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RubocopDirector
|
2
|
+
class FileStatsBuilder
|
3
|
+
include Dry::Monads[:result]
|
4
|
+
include Dry::Monads::Do.for(:build, :find_refactoring_value)
|
5
|
+
|
6
|
+
def initialize(rubocop_json:, update_counts:, config:)
|
7
|
+
@rubocop_json = rubocop_json
|
8
|
+
@update_counts = update_counts
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
file_stats = files_with_offenses.map do |file|
|
14
|
+
stats = {
|
15
|
+
path: file["path"],
|
16
|
+
updates_count: update_counts[file["path"]] || 0,
|
17
|
+
offense_counts: file["offenses"].group_by { |offense| offense["cop_name"] }.transform_values(&:count)
|
18
|
+
}
|
19
|
+
|
20
|
+
stats[:value] = yield find_refactoring_value(stats)
|
21
|
+
|
22
|
+
stats
|
23
|
+
end
|
24
|
+
|
25
|
+
Success(file_stats.sort_by { _1[:value] }.reverse)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :rubocop_json, :update_counts, :config
|
31
|
+
|
32
|
+
def files_with_offenses = rubocop_json.select { |file| file["offenses"].any? }
|
33
|
+
|
34
|
+
def find_refactoring_value(file)
|
35
|
+
update_weight = yield fetch_update_weight
|
36
|
+
|
37
|
+
offence_sum = file[:offense_counts].sum do |cop_name, count|
|
38
|
+
cop_weight = yield fetch_cop_weight(cop_name)
|
39
|
+
cop_weight * count
|
40
|
+
end
|
41
|
+
|
42
|
+
Success((offence_sum * file[:updates_count] * update_weight).to_f)
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch_cop_weight(cop_name)
|
46
|
+
weight = config.dig("weights", cop_name) || config["default_cop_weight"]
|
47
|
+
|
48
|
+
if weight
|
49
|
+
Success(weight)
|
50
|
+
else
|
51
|
+
Failure("could not find weight for #{cop_name} and `default_cop_weight` is not configured")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_update_weight
|
56
|
+
weight = config["update_weight"]
|
57
|
+
|
58
|
+
if weight
|
59
|
+
Success(weight)
|
60
|
+
else
|
61
|
+
Failure("`update_weight` is not configured")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "json"
|
2
|
+
require "optparse"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
require "dry/monads"
|
6
|
+
|
7
|
+
module RubocopDirector
|
8
|
+
class GitLogStats
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
def initialize(since)
|
12
|
+
@since = since
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch
|
16
|
+
stdout, stderr = Open3.capture3("git log --since=\"#{@since}\" --pretty=format: --name-only | sort | uniq -c | sort -rg")
|
17
|
+
|
18
|
+
return Failure("Failed to fetch git stats: #{stderr}") if stderr.length > 0
|
19
|
+
|
20
|
+
stats = stdout.split("\n")[1..].each_with_object({}) do |line, acc|
|
21
|
+
number, path = line.split
|
22
|
+
acc[path] = number.to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
Success(stats)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "json"
|
2
|
+
require "optparse"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
require "dry/monads"
|
6
|
+
|
7
|
+
module RubocopDirector
|
8
|
+
class OutputFormatter
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
def initialize(ranged_files:, since:)
|
12
|
+
@ranged_files = ranged_files
|
13
|
+
@since = since
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
result = @ranged_files.each_with_object([]) do |file, result|
|
18
|
+
result << "-" * 20
|
19
|
+
result << file[:path]
|
20
|
+
result << "updated #{file[:updates_count]} times since #{@since}"
|
21
|
+
result << "offences: #{file[:offense_counts].map { |cop, count| "#{cop} - #{count}" }.join(", ")}"
|
22
|
+
result << "refactoring value: #{file[:value]} (#{(100 * file[:value] / total_value.to_f).round(5)}%)"
|
23
|
+
end
|
24
|
+
|
25
|
+
Success(result)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def total_value = @ranged_files.sum { _1[:value] }
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "open3"
|
2
|
+
require "json"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
require "dry/monads"
|
6
|
+
|
7
|
+
module RubocopDirector
|
8
|
+
class RubocopStats
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
def fetch
|
12
|
+
_, stderr = Open3.capture3("sed '/todo/d' ./.rubocop.yml > tmpfile; mv tmpfile ./.rubocop.yml")
|
13
|
+
if stderr.length > 0
|
14
|
+
return Failure("Failed to remove TODO from rubocop config: #{stderr}")
|
15
|
+
end
|
16
|
+
|
17
|
+
stdout, stderr = Open3.capture3("bundle exec rubocop --format json")
|
18
|
+
|
19
|
+
if stderr.length > 0
|
20
|
+
Failure("Failed to fetch rubocop stats: #{stderr}")
|
21
|
+
else
|
22
|
+
Success(JSON.parse(stdout)["files"])
|
23
|
+
end
|
24
|
+
ensure
|
25
|
+
Open3.capture3("git checkout ./.rubocop.yml")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
require_relative "commands/generate_config"
|
4
|
+
require_relative "commands/plan"
|
5
|
+
|
6
|
+
module RubocopDirector
|
7
|
+
class Runner
|
8
|
+
def initialize(args)
|
9
|
+
arg_parser.parse(args)
|
10
|
+
@command ||= Commands::Plan.new(@since)
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
@command.run.either(
|
15
|
+
->(success_message) { puts success_message },
|
16
|
+
->(failure_message) { puts "\nFailure: #{failure_message}" }
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def arg_parser
|
23
|
+
OptionParser.new do |p|
|
24
|
+
p.on("--generate-config", "Generate default config based on .rubocop_todo.yml") do |since|
|
25
|
+
@command = Commands::GenerateConfig.new
|
26
|
+
end
|
27
|
+
|
28
|
+
p.on("--since=SINCE", "Specify date to start checking git history") do |since|
|
29
|
+
@since = since
|
30
|
+
end
|
31
|
+
|
32
|
+
p.on("--help", "Prints this help") do
|
33
|
+
puts p
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubocop_director
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- DmitryTsepelev
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-monads
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
description: Plan your refactorings properly.
|
28
|
+
email:
|
29
|
+
- dmitry.a.tsepelev@gmail.com
|
30
|
+
executables:
|
31
|
+
- rubocop-director
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".rspec"
|
36
|
+
- ".rubocop.yml"
|
37
|
+
- ".standard.yml"
|
38
|
+
- CHANGELOG.md
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE.txt
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- exe/rubocop-director
|
44
|
+
- lib/rubocop_director.rb
|
45
|
+
- lib/rubocop_director/commands/generate_config.rb
|
46
|
+
- lib/rubocop_director/commands/plan.rb
|
47
|
+
- lib/rubocop_director/file_stats_builder.rb
|
48
|
+
- lib/rubocop_director/git_log_stats.rb
|
49
|
+
- lib/rubocop_director/output_formatter.rb
|
50
|
+
- lib/rubocop_director/rubocop_stats.rb
|
51
|
+
- lib/rubocop_director/runner.rb
|
52
|
+
- lib/rubocop_director/version.rb
|
53
|
+
homepage: https://github.com/DmitryTsepelev/rubocop_director
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
metadata:
|
57
|
+
homepage_uri: https://github.com/DmitryTsepelev/rubocop_director
|
58
|
+
source_code_uri: https://github.com/DmitryTsepelev/rubocop_director
|
59
|
+
changelog_uri: https://github.com/DmitryTsepelev/rubocop_director/blob/master/CHANGELOG.md
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.0.0
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubygems_version: 3.2.15
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: Plan your refactorings properly.
|
79
|
+
test_files: []
|