rubocop_director 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9495c42ab7429abd8ad0cf4c31074079fba3419b9cccc56a9ab51fdd66ae3c49
4
- data.tar.gz: e634bc7e5f50acedb08be3c5dbb2f2e8ea03cbda5ef9694f89e87c5943b16a8d
3
+ metadata.gz: b39e90da2a16a400346bb55cf128eb323ac3feb04a617d43ed791e4b2eb6cfa7
4
+ data.tar.gz: b358b9f928eea4ce911e837837f19ec7be09d4e6467f1601c37f9e34250367dd
5
5
  SHA512:
6
- metadata.gz: 5ad6d7874bae6bc6bab004da66c96b777a24bec8f50457a7dc46aabb7b5e349bc49d52d2866e4f06c14c2ad71e9840d03c9fcc088d1d237061f80acfef3735f8
7
- data.tar.gz: 8b1aeda0559907b4696f47b77a069f605816e7d25b99318bb36e54288511c859350d144ddc8469c40e8c347afb0c2c16cbd0cd956d02200974703b9decbb04f4
6
+ metadata.gz: 2222f3a7fe1c71f335356877a6d8b9714e7ab54f787575d7cebdbd5009658aeeeac7778f6617044abe1d3f4796627c08e0fb91106bf27b2204e2c3541821b4d2
7
+ data.tar.gz: 4d090d5d404844a7b6c8b3dca91e34589bcf5f2f5585b8bf580bcbf759796af4ec6c53a7c6ae382e255a892ef0f3f3116aeadba471a32c974dedc58e1d0266aa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
- ## [Unreleased]
1
+ # Change log
2
2
 
3
- ## [0.1.0] - 2023-05-08
3
+ ## master
4
4
 
5
- - Initial release
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
- 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%)
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 (<number of offences> * <cop weight> * <number of file updates> * <update weight>)`.
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 insatll` to install dependencies
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
- todo = yield load_config
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
- weights = todo.keys.each_with_object({}).each do |cop, acc|
15
- acc.merge!(cop => 1)
16
- end
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
- # TODO: warn if file exists
19
- File.write(".rubocop-director.yml", {
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 || "1995-01-01"
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(CONFIG_NAME))
35
+ Success(YAML.load_file(@director_config_path))
34
36
  rescue Errno::ENOENT
35
- Failure("#{CONFIG_NAME} not found, generate it using `rubocop-director --generate-config`")
37
+ Failure("#{@director_config_path} not found, generate it using `rubocop-director --generate-config`")
36
38
  end
37
39
 
38
- def load_git_stats
39
- puts "💡 Checking git history since #{@since} to find hot files..."
40
- GitLogStats.new(@since).fetch
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 load_rubocop_json
44
- puts "💡🎥 Running rubocop to get the list of offences to fix..."
45
- RubocopStats.new.fetch
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
- RubocopDirector::FileStatsBuilder.new(rubocop_json: rubocop_json, update_counts: update_counts, config: config).build
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[:value] = yield find_refactoring_value(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
- update_weight = yield fetch_update_weight
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((offence_sum * file[:updates_count] * update_weight).to_f)
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 << "-" * 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)}%)"
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
- _, 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
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
- stdout, stderr = Open3.capture3("bundle exec rubocop --format json")
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
- ensure
25
- Open3.capture3("git checkout ./.rubocop.yml")
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
- arg_parser.parse(args)
10
- @command ||= Commands::Plan.new(@since)
10
+ @options = {}
11
+ arg_parser.parse(args, into: @options)
12
+ verify_options
11
13
  end
12
14
 
13
15
  def perform
14
- @command.run.either(
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.on("--generate-config", "Generate default config based on .rubocop_todo.yml") do |since|
25
- @command = Commands::GenerateConfig.new
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("--since=SINCE", "Specify date to start checking git history") do |since|
29
- @since = since
30
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubocopDirector
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -4,5 +4,7 @@ require "rubocop_director/version"
4
4
  require "rubocop_director/runner"
5
5
 
6
6
  module RubocopDirector
7
- CONFIG_NAME = ".rubocop-director.yml"
7
+ CONFIG_NAME = ".rubocop_director.yml"
8
+ RUBOCOP_CONFIG_NAME = ".rubocop.yml"
9
+ TODO_CONFIG_NAME = ".rubocop_todo.yml"
8
10
  end
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.1.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-05-13 00:00:00.000000000 Z
11
+ date: 2023-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-monads