rubocop_todo_corrector 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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).