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 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