rubocop_todo_corrector 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.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module RubocopTodoCorrector
6
+ module Commands
7
+ class Pick
8
+ class << self
9
+ # @param [String] mode
10
+ # @param [String] rubocop_todo_path
11
+ def call(
12
+ mode:,
13
+ rubocop_todo_path:
14
+ )
15
+ new(
16
+ mode: mode,
17
+ rubocop_todo_path: rubocop_todo_path
18
+ ).call
19
+ end
20
+ end
21
+
22
+ def initialize(
23
+ mode:,
24
+ rubocop_todo_path:
25
+ )
26
+ @mode = mode
27
+ @rubocop_todo_path = rubocop_todo_path
28
+ end
29
+
30
+ def call
31
+ check_rubocop_todo_existence
32
+ cop_name = picked_cop&.[](:name)
33
+ ::Kernel.puts cop_name if cop_name
34
+ end
35
+
36
+ private
37
+
38
+ # @return [Array<Hash>]
39
+ def auto_correctable_cops
40
+ rubocop_todo[:cops].select do |cop|
41
+ cop[:auto_correctable]
42
+ end
43
+ end
44
+
45
+ def check_rubocop_todo_existence
46
+ raise "#{rubocop_todo_pathname.to_s.inspect} does not exist." unless rubocop_todo_pathname.exist?
47
+ end
48
+
49
+ # @return [Hash, nil]
50
+ def picked_cop
51
+ case @mode
52
+ when 'first'
53
+ auto_correctable_cops.first
54
+ when 'last'
55
+ auto_correctable_cops.last
56
+ when 'least_occured'
57
+ auto_correctable_cops.min_by do |cop|
58
+ cop[:offenses_count]
59
+ end
60
+ when 'most_occured'
61
+ auto_correctable_cops.max_by do |cop|
62
+ cop[:offenses_count]
63
+ end
64
+ else
65
+ auto_correctable_cops.sample
66
+ end
67
+ end
68
+
69
+ def rubocop_todo
70
+ RubocopTodoParser.call(content: rubocop_todo_content)
71
+ end
72
+
73
+ # @return [String]
74
+ def rubocop_todo_content
75
+ rubocop_todo_pathname.read
76
+ end
77
+
78
+ # @return [Pathname]
79
+ def rubocop_todo_pathname
80
+ @rubocop_todo_pathname ||= ::Pathname.new(@rubocop_todo_path)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module RubocopTodoCorrector
6
+ module Commands
7
+ class Remove
8
+ class << self
9
+ # @param [String] cop_name
10
+ # @param [String] rubocop_todo_path
11
+ def call(
12
+ cop_name:,
13
+ rubocop_todo_path:
14
+ )
15
+ new(
16
+ cop_name: cop_name,
17
+ rubocop_todo_path: rubocop_todo_path
18
+ ).call
19
+ end
20
+ end
21
+
22
+ def initialize(
23
+ cop_name:,
24
+ rubocop_todo_path:
25
+ )
26
+ @cop_name = cop_name
27
+ @rubocop_todo_path = rubocop_todo_path
28
+ end
29
+
30
+ # @return [String]
31
+ def call
32
+ check_rubocop_todo_existence
33
+ write
34
+ end
35
+
36
+ private
37
+
38
+ def check_rubocop_todo_existence
39
+ raise "#{rubocop_todo_pathname.to_s.inspect} does not exist." unless rubocop_todo_pathname.exist?
40
+ end
41
+
42
+ # @return [String]
43
+ def processed_content
44
+ rubocop_todo_content.split("\n\n").grep_v(/^#{@cop_name}:$/).join("\n\n")
45
+ end
46
+
47
+ def rubocop_todo
48
+ RubocopTodoParser.call(content: rubocop_todo_content)
49
+ end
50
+
51
+ # @return [String]
52
+ def rubocop_todo_content
53
+ rubocop_todo_pathname.read
54
+ end
55
+
56
+ # @return [Pathname]
57
+ def rubocop_todo_pathname
58
+ @rubocop_todo_pathname ||= ::Pathname.new(@rubocop_todo_path)
59
+ end
60
+
61
+ def write
62
+ ::File.write(
63
+ @rubocop_todo_path,
64
+ processed_content
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubocopTodoCorrector
4
+ module Commands
5
+ autoload :Bundle, 'rubocop_todo_corrector/commands/bundle'
6
+ autoload :Describe, 'rubocop_todo_corrector/commands/describe'
7
+ autoload :Generate, 'rubocop_todo_corrector/commands/generate'
8
+ autoload :Pick, 'rubocop_todo_corrector/commands/pick'
9
+ autoload :Remove, 'rubocop_todo_corrector/commands/remove'
10
+ end
11
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubocopTodoCorrector
4
+ class CopDocumentParser
5
+ class << self
6
+ # @param [String] source_path
7
+ # @return [Hash, nil]
8
+ def call(
9
+ source_path:
10
+ )
11
+ new(
12
+ source_path: source_path
13
+ ).call
14
+ end
15
+ end
16
+
17
+ def initialize(
18
+ source_path:
19
+ )
20
+ @source_path = source_path
21
+ end
22
+
23
+ # @return [Hash, nil]
24
+ def call
25
+ return unless yard_class_object
26
+
27
+ {
28
+ description: yard_class_object.docstring,
29
+ examples: yard_class_object.tags('example').map do |tag|
30
+ {
31
+ name: tag.name,
32
+ text: tag.text
33
+ }
34
+ end
35
+ }
36
+ end
37
+
38
+ private
39
+
40
+ # @return [YARD::CodeObjects::ClassObject]
41
+ def yard_class_object
42
+ @yard_class_object ||= begin
43
+ ::YARD.parse(@source_path)
44
+ yardoc = ::YARD::Registry.all(:class).first
45
+ ::YARD::Registry.clear
46
+ yardoc
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module RubocopTodoCorrector
6
+ class CopSourceDetector
7
+ class << self
8
+ # @param [String] cop_name
9
+ # @param [String] temporary_gemfile_path
10
+ # @return [String, nil]
11
+ def call(
12
+ cop_name:,
13
+ temporary_gemfile_path:
14
+ )
15
+ new(
16
+ cop_name: cop_name,
17
+ temporary_gemfile_path: temporary_gemfile_path
18
+ ).call
19
+ end
20
+ end
21
+
22
+ def initialize(
23
+ cop_name:,
24
+ temporary_gemfile_path:
25
+ )
26
+ @cop_name = cop_name
27
+ @temporary_gemfile_path = temporary_gemfile_path
28
+ end
29
+
30
+ # @return [String, nil]
31
+ def call
32
+ output = capture
33
+ output unless output.empty?
34
+ end
35
+
36
+ private
37
+
38
+ # @return [String]
39
+ def capture
40
+ ::Open3.capture2(
41
+ { 'BUNDLE_GEMFILE' => @temporary_gemfile_path },
42
+ "bundle exec ruby -e 'Bundler.require; print Object.const_source_location(#{cop_class_name.inspect}).first'"
43
+ ).first
44
+ end
45
+
46
+ # @return [String]
47
+ def cop_class_name
48
+ "RuboCop::Cop::#{@cop_name.sub('/', '::')}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module RubocopTodoCorrector
6
+ class DescriptionRenderer
7
+ TEMPLATE_PATH = ::File.expand_path('../../templates/description.md.erb', __dir__)
8
+
9
+ class << self
10
+ # @param [String] cop_document
11
+ # @param [String] cop_name
12
+ # @param [String] cop_source_path
13
+ # @return [String]
14
+ def call(
15
+ cop_document:,
16
+ cop_name:,
17
+ cop_source_path:
18
+ )
19
+ new(
20
+ cop_document: cop_document,
21
+ cop_name: cop_name,
22
+ cop_source_path: cop_source_path
23
+ ).call
24
+ end
25
+ end
26
+
27
+ def initialize(
28
+ cop_document:,
29
+ cop_name:,
30
+ cop_source_path:
31
+ )
32
+ @cop_document = cop_document
33
+ @cop_name = cop_name
34
+ @cop_source_path = cop_source_path
35
+ end
36
+
37
+ # @return [String]
38
+ def call
39
+ ::ERB.new(template_content, trim_mode: '-').result(binding)
40
+ end
41
+
42
+ private
43
+
44
+ # @return [String]
45
+ def cop_url
46
+ if gem_name
47
+ "https://www.rubydoc.info/gems/#{gem_name}/RuboCop/Cop/#{@cop_name}"
48
+ else
49
+ "https://www.google.com/search?q=rubocop+#{::URI.encode_www_form_component(@cop_name.inspect)}"
50
+ end
51
+ end
52
+
53
+ # @return [String, nil]
54
+ def gem_name
55
+ @gem_name ||= @cop_source_path[
56
+ %r{/gems/([\w-]+)-\d+(?:\.\w+)*/lib},
57
+ 1
58
+ ]
59
+ end
60
+
61
+ # @return [String]
62
+ def template_content
63
+ ::File.read(TEMPLATE_PATH)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module RubocopTodoCorrector
6
+ class GemNamesDetector
7
+ class << self
8
+ # @param [String] rubocop_configuration_path
9
+ # @return [Array<String>]
10
+ def call(rubocop_configuration_path:)
11
+ new(rubocop_configuration_path: rubocop_configuration_path).call
12
+ end
13
+ end
14
+
15
+ def initialize(rubocop_configuration_path:)
16
+ @rubocop_configuration_path = rubocop_configuration_path
17
+ end
18
+
19
+ def call
20
+ gem_names
21
+ end
22
+
23
+ private
24
+
25
+ # @return [Hash]
26
+ def configuration_hash
27
+ ::YAML.load_file(@rubocop_configuration_path)
28
+ end
29
+
30
+ # @return [Array<String>]
31
+ def gem_names
32
+ requires.grep(/\A[\w-]+\z/)
33
+ end
34
+
35
+ # @return [Array<String>]
36
+ def requires
37
+ configuration_hash['require'] || []
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+
5
+ module RubocopTodoCorrector
6
+ class GemVersionDetector
7
+ class << self
8
+ # @param [String] gemfile_lock_path
9
+ # @param [String] name
10
+ # @return [String, nil]
11
+ def call(gemfile_lock_path:, gem_name:)
12
+ new(
13
+ gemfile_lock_path: gemfile_lock_path,
14
+ gem_name: gem_name
15
+ ).call
16
+ end
17
+ end
18
+
19
+ def initialize(gemfile_lock_path:, gem_name:)
20
+ @gemfile_lock_path = gemfile_lock_path
21
+ @gem_name = gem_name
22
+ end
23
+
24
+ # @return [String, nil]
25
+ def call
26
+ specification&.version&.to_s
27
+ end
28
+
29
+ private
30
+
31
+ # @return [Bundler::LockfileParser]
32
+ def bundler_lockfile_parser
33
+ ::Bundler::LockfileParser.new(gemfile_lock_content)
34
+ end
35
+
36
+ # @return [String]
37
+ def gemfile_lock_content
38
+ ::File.read(@gemfile_lock_path)
39
+ end
40
+
41
+ # @return [Bundler::LazySpecification]
42
+ def specification
43
+ specs.find do |specification|
44
+ specification.name == @gem_name
45
+ end
46
+ end
47
+
48
+ # @return [Array<Bundler::LazySpecification>]
49
+ def specs
50
+ bundler_lockfile_parser.specs
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubocopTodoCorrector
4
+ class RubocopTodoParser
5
+ COP_NAME_LINE_REGEXP = %r{
6
+ ^
7
+ (?<cop_name>
8
+ \w+
9
+ (?:/\w+)*
10
+ )
11
+ :
12
+ $
13
+ }x
14
+
15
+ class << self
16
+ # @param [String] content
17
+ def call(content:)
18
+ new(
19
+ content: content
20
+ ).call
21
+ end
22
+ end
23
+
24
+ def initialize(content:)
25
+ @content = content
26
+ end
27
+
28
+ # @return [Hash]
29
+ def call
30
+ {
31
+ cops: cops,
32
+ previous_rubocop_command: previous_rubocop_command
33
+ }
34
+ end
35
+
36
+ private
37
+
38
+ # @return [Array<Hash>]
39
+ def cops
40
+ cop_sections.map do |cop_section|
41
+ RubocopTodoSectionParser.call(content: cop_section)
42
+ end
43
+ end
44
+
45
+ def previous_rubocop_command
46
+ @content[
47
+ /`(.+)`/,
48
+ 1
49
+ ]
50
+ end
51
+
52
+ # @return [Array<String>]
53
+ def cop_sections
54
+ @content.split("\n\n").grep(COP_NAME_LINE_REGEXP)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubocopTodoCorrector
4
+ class RubocopTodoSectionParser
5
+ class << self
6
+ # @param [String] content
7
+ def call(content:)
8
+ new(
9
+ content: content
10
+ ).call
11
+ end
12
+ end
13
+
14
+ def initialize(content:)
15
+ @content = content
16
+ end
17
+
18
+ # @return [Hash]
19
+ def call
20
+ {
21
+ auto_correctable: auto_correctable?,
22
+ name: name,
23
+ offenses_count: offenses_count
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ # @return [Boolean]
30
+ def auto_correctable?
31
+ @content.include?('# Cop supports --auto-correct.') ||
32
+ @content.include?('# Cop supports safe auto-correction.') ||
33
+ @content.include?('# Cop supports unsafe auto-correction.')
34
+ end
35
+
36
+ # @return [String, nil]
37
+ def name
38
+ @content[/(#{RubocopTodoParser::COP_NAME_LINE_REGEXP})/, 'cop_name']
39
+ end
40
+
41
+ # @return [Integer, nil]
42
+ def offenses_count
43
+ @content[
44
+ /^# Offense count: (?<offenses_count>\d+)$/,
45
+ 'offenses_count'
46
+ ]&.to_i
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubocopTodoCorrector
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rubocop_todo_corrector/version'
4
+
5
+ module RubocopTodoCorrector
6
+ autoload :BundleInstaller, 'rubocop_todo_corrector/bundle_installer'
7
+ autoload :Cli, 'rubocop_todo_corrector/cli'
8
+ autoload :Commands, 'rubocop_todo_corrector/commands'
9
+ autoload :CopDocumentParser, 'rubocop_todo_corrector/cop_document_parser'
10
+ autoload :CopSourceDetector, 'rubocop_todo_corrector/cop_source_detector'
11
+ autoload :DescriptionRenderer, 'rubocop_todo_corrector/description_renderer'
12
+ autoload :GemNamesDetector, 'rubocop_todo_corrector/gem_names_detector'
13
+ autoload :GemVersionDetector, 'rubocop_todo_corrector/gem_version_detector'
14
+ autoload :RubocopTodoParser, 'rubocop_todo_corrector/rubocop_todo_parser'
15
+ autoload :RubocopTodoSectionParser, 'rubocop_todo_corrector/rubocop_todo_section_parser'
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/rubocop_todo_corrector/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rubocop_todo_corrector'
7
+ spec.version = RubocopTodoCorrector::VERSION
8
+ spec.authors = ['Ryo Nakamura']
9
+ spec.email = ['r7kamura@gmail.com']
10
+
11
+ spec.summary = 'Auto-correct offenses defined in .rubocop_todo.yml.'
12
+ spec.homepage = 'https://github.com/r7kamura/rubocop_todo_corrector'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 3.0'
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = spec.homepage
18
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+ spec.metadata['rubygems_mfa_required'] = 'true'
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_dependency 'bundler'
33
+ spec.add_dependency 'thor'
34
+ spec.add_dependency 'yard'
35
+ end
@@ -0,0 +1,29 @@
1
+ ## Summary
2
+
3
+ Auto-corrected [<%= @cop_name %>](<%= cop_url %>).
4
+
5
+ ## Details
6
+
7
+ ### <%= @cop_name %>
8
+
9
+ <% @cop_document[:description].each_line do |line| -%>
10
+ <%= "> #{line}".strip %>
11
+ <% end -%>
12
+ <% unless @cop_document[:examples].empty? -%>
13
+ >
14
+ > #### Examples
15
+ <% @cop_document[:examples].each do |example| -%>
16
+ >
17
+ > <%= example[:name].strip %>
18
+ >
19
+ > ```ruby
20
+ <% example[:text].each_line do |line| -%>
21
+ <%= "> #{line}".strip %>
22
+ <% end -%>
23
+ > ```
24
+ <% end -%>
25
+ <% end -%>
26
+
27
+ ### Note
28
+
29
+ This pull request was generated by [rubocop_todo_corrector](https://github.com/r7kamura/rubocop_todo_corrector).