rubocop_director 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -3
- data/README.md +15 -13
- data/lib/rubocop_director/commands/generate_config.rb +36 -14
- data/lib/rubocop_director/commands/plan.rb +14 -12
- data/lib/rubocop_director/file_stats_builder.rb +11 -5
- data/lib/rubocop_director/output_formatter.rb +7 -5
- data/lib/rubocop_director/rubocop_stats.rb +37 -7
- data/lib/rubocop_director/runner.rb +38 -8
- data/lib/rubocop_director/version.rb +1 -1
- data/lib/rubocop_director.rb +3 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b39e90da2a16a400346bb55cf128eb323ac3feb04a617d43ed791e4b2eb6cfa7
|
4
|
+
data.tar.gz: b358b9f928eea4ce911e837837f19ec7be09d4e6467f1601c37f9e34250367dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2222f3a7fe1c71f335356877a6d8b9714e7ab54f787575d7cebdbd5009658aeeeac7778f6617044abe1d3f4796627c08e0fb91106bf27b2204e2c3541821b4d2
|
7
|
+
data.tar.gz: 4d090d5d404844a7b6c8b3dca91e34589bcf5f2f5585b8bf580bcbf759796af4ec6c53a7c6ae382e255a892ef0f3f3116aeadba471a32c974dedc58e1d0266aa
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
|
-
|
1
|
+
# Change log
|
2
2
|
|
3
|
-
##
|
3
|
+
## master
|
4
4
|
|
5
|
-
|
5
|
+
## 0.3.0 (2023-06-03)
|
6
|
+
|
7
|
+
- [PR #12](https://github.com/DmitryTsepelev/rubocop_director/pull/12) Added option to pass configs as args for all config files ([@samarthkathal][])
|
8
|
+
|
9
|
+
## 0.2.0 (2023-06-03)
|
10
|
+
|
11
|
+
- [PR #13](https://github.com/DmitryTsepelev/rubocop_director/pull/13) Prettify output formatter and change stats calculation algorithm ([@DmitryTsepelev][])
|
12
|
+
- [PR #9](https://github.com/DmitryTsepelev/rubocop_director/pull/9) Removed sed dependency: stats generated using temp config ([@samarthkathal][])
|
13
|
+
- [PR #8](https://github.com/DmitryTsepelev/rubocop_director/pull/8) Reorder plan flow to run expensive check first ([@DmitryTsepelev][])
|
14
|
+
- [PR #7](https://github.com/DmitryTsepelev/rubocop_director/pull/7) GenerateConfig will a prompt to overwrite when config already exists ([@samarthkathal][])
|
15
|
+
|
16
|
+
## 0.1.0 (2023-05-08)
|
17
|
+
|
18
|
+
- Initial version ([@DmitryTsepelev][])
|
19
|
+
|
20
|
+
[@DmitryTsepelev]: https://github.com/DmitryTsepelev
|
21
|
+
[@samarthkathal]: https://github.com/samarthkathal
|
data/README.md
CHANGED
@@ -6,7 +6,6 @@ A command–line utility for refactoring planning. It uses `.rubocop_todo.yml` a
|
|
6
6
|
|
7
7
|
Prerequisites:
|
8
8
|
|
9
|
-
- `sed`;
|
10
9
|
- `git` repo;
|
11
10
|
- generated `.rubocop_todo.yml`.
|
12
11
|
|
@@ -44,21 +43,24 @@ As a result you'll get something like this:
|
|
44
43
|
💡 Checking git history since 1995-01-01 to find hot files...
|
45
44
|
💡🎥 Running rubocop to get the list of offences to fix...
|
46
45
|
💡🎥🎬 Calculating a list of files to refactor...
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
46
|
+
|
47
|
+
Path: app/controllers/user_controller.rb
|
48
|
+
Updated 99 times since 2023-01-01
|
49
|
+
Offenses:
|
50
|
+
🚓 Rails/SomeCop - 2
|
51
|
+
Refactoring value: 1.5431217598108933 (54.79575%)
|
52
|
+
|
53
|
+
Path: app/models/user.rb
|
54
|
+
Updated 136 times since 2023-01-01
|
55
|
+
Offenses:
|
56
|
+
🚓 Rails/SomeCop - 1
|
57
|
+
🚓 Rails/AnotherCop - 1
|
58
|
+
Refactoring value: 1.2730122208719792 (45.20425%)
|
57
59
|
```
|
58
60
|
|
59
61
|
> Want a different output format (e.g., CSV)? Let me know, open an issue!
|
60
62
|
|
61
|
-
Value is calculated using a formula: `sum of value from each cop (<
|
63
|
+
Value is calculated using a formula: `sum of value from each cop (<count of offences> * <cop weight> * (<count of file updates> / <total count of updates>) ** <update weight>)`.
|
62
64
|
|
63
65
|
If you need to count updates from a specific date—use `--since`:
|
64
66
|
|
@@ -68,7 +70,7 @@ bundle exec rubocop-director --since=2023-01-01
|
|
68
70
|
|
69
71
|
## Development
|
70
72
|
|
71
|
-
After checking out the repo, run `bundle
|
73
|
+
After checking out the repo, run `bundle install` to install dependencies
|
72
74
|
|
73
75
|
## Contributing
|
74
76
|
|
@@ -8,15 +8,45 @@ module RubocopDirector
|
|
8
8
|
|
9
9
|
RUBOCOP_TODO = ".rubocop_todo.yml"
|
10
10
|
|
11
|
+
def initialize(director_config:)
|
12
|
+
@director_config_path = director_config
|
13
|
+
@todo_config_path = TODO_CONFIG_NAME
|
14
|
+
end
|
15
|
+
|
11
16
|
def run
|
12
|
-
|
17
|
+
rubocop_todo = yield load_config
|
18
|
+
yield check_config_already_exists
|
19
|
+
|
20
|
+
create_config(rubocop_todo)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
13
24
|
|
14
|
-
|
15
|
-
|
16
|
-
|
25
|
+
def load_config
|
26
|
+
Success(YAML.load_file(@todo_config_path))
|
27
|
+
rescue Errno::ENOENT
|
28
|
+
Failure("#{@todo_config_path} not found, generate it using `rubocop --regenerate-todo`")
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_config_already_exists
|
32
|
+
return Success() if config_not_exists? || override_config?
|
33
|
+
|
34
|
+
Failure("previous version of #{@director_config_path} was preserved.")
|
35
|
+
end
|
17
36
|
|
18
|
-
|
19
|
-
File.
|
37
|
+
def config_not_exists?
|
38
|
+
!File.file?(@director_config_path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def override_config?
|
42
|
+
puts("#{@director_config_path} already exists, do you want to override it? (y/n)")
|
43
|
+
$stdin.gets.chomp == "y"
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_config(rubocop_todo)
|
47
|
+
weights = rubocop_todo.keys.to_h { |key| [key, 1] }
|
48
|
+
|
49
|
+
File.write(@director_config_path, {
|
20
50
|
"update_weight" => 1,
|
21
51
|
"default_cop_weight" => 1,
|
22
52
|
"weights" => weights
|
@@ -24,14 +54,6 @@ module RubocopDirector
|
|
24
54
|
|
25
55
|
Success("Config generated")
|
26
56
|
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
57
|
end
|
36
58
|
end
|
37
59
|
end
|
@@ -14,14 +14,16 @@ module RubocopDirector
|
|
14
14
|
include Dry::Monads[:result]
|
15
15
|
include Dry::Monads::Do.for(:run)
|
16
16
|
|
17
|
-
def initialize(since)
|
18
|
-
@since = since
|
17
|
+
def initialize(director_config:, rubocop_config:, since: "1995-01-01")
|
18
|
+
@since = since.to_s
|
19
|
+
@director_config_path = director_config
|
20
|
+
@rubocop_config_path = rubocop_config
|
19
21
|
end
|
20
22
|
|
21
23
|
def run
|
22
24
|
config = yield load_config
|
23
|
-
update_counts = yield load_git_stats
|
24
25
|
rubocop_json = yield load_rubocop_json
|
26
|
+
update_counts = yield load_git_stats
|
25
27
|
ranged_files = yield range_files(rubocop_json: rubocop_json, update_counts: update_counts, config: config)
|
26
28
|
|
27
29
|
OutputFormatter.new(ranged_files: ranged_files, since: @since).call
|
@@ -30,24 +32,24 @@ module RubocopDirector
|
|
30
32
|
private
|
31
33
|
|
32
34
|
def load_config
|
33
|
-
Success(YAML.load_file(
|
35
|
+
Success(YAML.load_file(@director_config_path))
|
34
36
|
rescue Errno::ENOENT
|
35
|
-
Failure("#{
|
37
|
+
Failure("#{@director_config_path} not found, generate it using `rubocop-director --generate-config`")
|
36
38
|
end
|
37
39
|
|
38
|
-
def
|
39
|
-
puts "💡
|
40
|
-
|
40
|
+
def load_rubocop_json
|
41
|
+
puts "💡 Running rubocop to get the list of offences to fix..."
|
42
|
+
RubocopStats.new(@rubocop_config_path).fetch
|
41
43
|
end
|
42
44
|
|
43
|
-
def
|
44
|
-
puts "💡🎥
|
45
|
-
|
45
|
+
def load_git_stats
|
46
|
+
puts "💡🎥 Checking git history since #{@since} to find hot files..."
|
47
|
+
GitLogStats.new(@since).fetch
|
46
48
|
end
|
47
49
|
|
48
50
|
def range_files(rubocop_json:, update_counts:, config:)
|
49
51
|
puts "💡🎥🎬 Calculating a list of files to refactor..."
|
50
|
-
|
52
|
+
FileStatsBuilder.new(rubocop_json: rubocop_json, update_counts: update_counts, config: config).build
|
51
53
|
end
|
52
54
|
end
|
53
55
|
end
|
@@ -10,6 +10,8 @@ module RubocopDirector
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def build
|
13
|
+
update_weight = yield fetch_update_weight
|
14
|
+
|
13
15
|
file_stats = files_with_offenses.map do |file|
|
14
16
|
stats = {
|
15
17
|
path: file["path"],
|
@@ -17,11 +19,17 @@ module RubocopDirector
|
|
17
19
|
offense_counts: file["offenses"].group_by { |offense| offense["cop_name"] }.transform_values(&:count)
|
18
20
|
}
|
19
21
|
|
20
|
-
stats[:
|
22
|
+
stats[:cop_value] = yield find_refactoring_value(stats)
|
21
23
|
|
22
24
|
stats
|
23
25
|
end
|
24
26
|
|
27
|
+
total_updates_count = file_stats.sum { |f| f[:updates_count] }
|
28
|
+
|
29
|
+
file_stats = file_stats.each do |stats|
|
30
|
+
stats[:value] = stats[:cop_value] * ((stats[:updates_count] / total_updates_count.to_f)**update_weight)
|
31
|
+
end
|
32
|
+
|
25
33
|
Success(file_stats.sort_by { _1[:value] }.reverse)
|
26
34
|
end
|
27
35
|
|
@@ -32,14 +40,12 @@ module RubocopDirector
|
|
32
40
|
def files_with_offenses = rubocop_json.select { |file| file["offenses"].any? }
|
33
41
|
|
34
42
|
def find_refactoring_value(file)
|
35
|
-
|
36
|
-
|
37
|
-
offence_sum = file[:offense_counts].sum do |cop_name, count|
|
43
|
+
value = file[:offense_counts].sum do |cop_name, count|
|
38
44
|
cop_weight = yield fetch_cop_weight(cop_name)
|
39
45
|
cop_weight * count
|
40
46
|
end
|
41
47
|
|
42
|
-
Success(
|
48
|
+
Success(value)
|
43
49
|
end
|
44
50
|
|
45
51
|
def fetch_cop_weight(cop_name)
|
@@ -15,11 +15,13 @@ module RubocopDirector
|
|
15
15
|
|
16
16
|
def call
|
17
17
|
result = @ranged_files.each_with_object([]) do |file, result|
|
18
|
-
result << "
|
19
|
-
|
20
|
-
result << "
|
21
|
-
result << "
|
22
|
-
result << "
|
18
|
+
result << ""
|
19
|
+
|
20
|
+
result << "Path: #{file[:path]}"
|
21
|
+
result << "Updated #{file[:updates_count]} times since #{@since}"
|
22
|
+
result << "Offenses:"
|
23
|
+
file[:offense_counts].each { |cop, count| result << " 🚓 #{cop} - #{count}" }
|
24
|
+
result << "Refactoring value: #{file[:value]} (#{(100 * file[:value] / total_value.to_f).round(5)}%)"
|
23
25
|
end
|
24
26
|
|
25
27
|
Success(result)
|
@@ -7,22 +7,52 @@ require "dry/monads"
|
|
7
7
|
module RubocopDirector
|
8
8
|
class RubocopStats
|
9
9
|
include Dry::Monads[:result]
|
10
|
+
include Dry::Monads::Do.for(:fetch)
|
11
|
+
|
12
|
+
TEMP_CONFIG_PATH = "./.temp_rubocop.yml"
|
13
|
+
|
14
|
+
def initialize(rubocop_path)
|
15
|
+
@rubocop_path = rubocop_path
|
16
|
+
end
|
10
17
|
|
11
18
|
def fetch
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
19
|
+
config = yield load_config
|
20
|
+
yield generate_temp_rubocop_config_without_todo(initial_config: config)
|
21
|
+
|
22
|
+
stats = yield generate_stats
|
23
|
+
yield remove_temp_config
|
24
|
+
|
25
|
+
Success(stats)
|
26
|
+
end
|
16
27
|
|
17
|
-
|
28
|
+
private
|
29
|
+
|
30
|
+
def load_config
|
31
|
+
Success(YAML.load_file(@rubocop_path))
|
32
|
+
rescue Errno::ENOENT
|
33
|
+
Failure("unable to load rubocop config. Please ensure .rubocop.yml file is present at your project's root directory")
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_temp_rubocop_config_without_todo(initial_config:)
|
37
|
+
initial_config.dig("inherit_from")&.delete(".rubocop_todo.yml")
|
38
|
+
|
39
|
+
Success(File.write(TEMP_CONFIG_PATH, initial_config.to_yaml))
|
40
|
+
rescue IOError => e
|
41
|
+
Failure("Failed to create a temporary config file to generate stats: #{e}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate_stats
|
45
|
+
stdout, stderr = Open3.capture3("bundle exec rubocop -c #{TEMP_CONFIG_PATH} --format json")
|
18
46
|
|
19
47
|
if stderr.length > 0
|
20
48
|
Failure("Failed to fetch rubocop stats: #{stderr}")
|
21
49
|
else
|
22
50
|
Success(JSON.parse(stdout)["files"])
|
23
51
|
end
|
24
|
-
|
25
|
-
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_temp_config
|
55
|
+
Success(File.delete(TEMP_CONFIG_PATH))
|
26
56
|
end
|
27
57
|
end
|
28
58
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "optparse"
|
2
|
+
require "optparse/date"
|
2
3
|
|
3
4
|
require_relative "commands/generate_config"
|
4
5
|
require_relative "commands/plan"
|
@@ -6,12 +7,13 @@ require_relative "commands/plan"
|
|
6
7
|
module RubocopDirector
|
7
8
|
class Runner
|
8
9
|
def initialize(args)
|
9
|
-
|
10
|
-
|
10
|
+
@options = {}
|
11
|
+
arg_parser.parse(args, into: @options)
|
12
|
+
verify_options
|
11
13
|
end
|
12
14
|
|
13
15
|
def perform
|
14
|
-
|
16
|
+
command.run.either(
|
15
17
|
->(success_message) { puts success_message },
|
16
18
|
->(failure_message) { puts "\nFailure: #{failure_message}" }
|
17
19
|
)
|
@@ -19,15 +21,43 @@ module RubocopDirector
|
|
19
21
|
|
20
22
|
private
|
21
23
|
|
24
|
+
def verify_options
|
25
|
+
@options[:rubocop_config] = verified_path(config_name: RUBOCOP_CONFIG_NAME, path: @options[:rubocop_config])
|
26
|
+
@options[:director_config] = verified_path(config_name: CONFIG_NAME, path: @options[:director_config])
|
27
|
+
end
|
28
|
+
|
29
|
+
def verified_path(config_name:, path:)
|
30
|
+
path = project_root if path.nil?
|
31
|
+
path.directory? ? path + config_name : path
|
32
|
+
end
|
33
|
+
|
34
|
+
def project_root
|
35
|
+
return Rails.root if defined?(Rails)
|
36
|
+
return Bundler.root if defined?(Bundler)
|
37
|
+
|
38
|
+
Pathname.new(Dir.pwd)
|
39
|
+
end
|
40
|
+
|
41
|
+
def command
|
42
|
+
@command ||= if @options[:generate_config]
|
43
|
+
Commands::GenerateConfig.new(**@options.slice(:director_config))
|
44
|
+
else
|
45
|
+
Commands::Plan.new(**@options.slice(:since, :director_config, :rubocop_config))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
22
49
|
def arg_parser
|
23
50
|
OptionParser.new do |p|
|
24
|
-
p.
|
25
|
-
|
51
|
+
p.accept(Pathname) do |s|
|
52
|
+
Pathname.new(s)
|
53
|
+
rescue ArgumentError, TypeError
|
54
|
+
raise OptionParser::InvalidArgument, s
|
26
55
|
end
|
27
56
|
|
28
|
-
p.on("--
|
29
|
-
|
30
|
-
|
57
|
+
p.on("--generate_config", "Generate default config based on .rubocop_todo.yml")
|
58
|
+
p.on("--since=SINCE", Date, "Specify date to start checking git history")
|
59
|
+
p.on("--director_config=PATH", Pathname, "Specify path where .rubocop_director.yml config must be read from OR written to, default path: {PROJECTROOT}/.rubocop_director.yml")
|
60
|
+
p.on("--rubocop_config=PATH", Pathname, "Specify path where .rubocop.yml config must be read from, default path: {PROJECTROOT}/.rubocop.yml")
|
31
61
|
|
32
62
|
p.on("--help", "Prints this help") do
|
33
63
|
puts p
|
data/lib/rubocop_director.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubocop_director
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- DmitryTsepelev
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-monads
|