rubofix 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7e8ee1ffa530c675aa16a7776c5352e7b31fc6f560180d8746703b2a3374edb2
4
+ data.tar.gz: 7a9e6c9b061b771c9b98a4e567cd9b2cbffffa4226df2548b0f08c9571a21355
5
+ SHA512:
6
+ metadata.gz: 15734284e3c48fafedea3daf41968975d2d9309721ff32260d53cbbf24a582211855e85fb79f970a86889eb8cb40b5d7c103d4256f6d7ec6b396c82118503553
7
+ data.tar.gz: 5006c77771dc74861dc7b660813d962e0bbdc6e656015b0536bd5630d7e528c59e0f2bb2d8c446796662531c3ce294866a057602f77d9b3c943b523a86a6a8e3
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2013 Michael Grosser <michael@grosser.it>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/rubofix ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # enable local usage from cloned repo
5
+ root = File.expand_path('..', __dir__)
6
+ $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile")
7
+
8
+ require "rubofix"
9
+ require "rubofix/cli"
10
+
11
+ Rubofix::CLI.new.run(ARGV)
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ class Rubofix
6
+ class CLI
7
+ def run(argv)
8
+ # get config first so we fail fast
9
+ api_key = ENV.fetch("RUBOFIX_API_KEY")
10
+ url = ENV.fetch("RUBOFIX_URL", "https://api.openai.com")
11
+ model = ENV.fetch("MODEL", "gpt-4o-mini")
12
+ max = Integer(ENV.fetch("MAX", "1"))
13
+ context = Integer(ENV.fetch("CONTEXT", "0"))
14
+
15
+ # autocorrect
16
+ puts "Attempting to use builtin autocorrect ..."
17
+ command = "bundle exec rubocop --autocorrect-all #{argv.shelljoin}"
18
+ puts command if ENV["DEBUG"]
19
+ output = `#{command}`
20
+ puts output if ENV["DEBUG"]
21
+ return 0 if $?.success? # nothing to fix
22
+
23
+ # get remaining warnings from rubocop
24
+ puts "Getting remaining offenses ..."
25
+ command = "bundle exec rubocop --parallel #{argv.shelljoin}"
26
+ output = remove_shell_colors(`#{command}`)
27
+ return 0 if $?.success? # nothing to fix
28
+
29
+ # parse rubocop output
30
+ # output is spam,warnings,spam and we only want warnings
31
+ # warnings format is warning\nline\npointer but we only need the warning
32
+ offenses = output.split("\n\n")[2].split("\n").each_slice(3)
33
+ abort "Unparseable offenses found with command:\n#{command}\n#{output}" if offenses.any? { |w| w.size != 3 }
34
+ offenses = offenses.map(&:first)
35
+ abort "No offenses found\n#{output}" if offenses.empty?
36
+
37
+ # fix offenses (in reverse order so line numbers stay correct)
38
+ puts "Fixing MAX=#{max} of #{offenses.size} offenses with MODEL=#{model} ..."
39
+ rubofix = Rubofix.new(url:, api_key:, model:, context:)
40
+ offenses.reverse.first(max).each do |warning|
41
+ rubofix.fix! warning
42
+ end
43
+
44
+ # don't let users that do not read just assume everything is fine
45
+ if offenses.size > max
46
+ warn "Not all offenses fixed, run again to fix more"
47
+ return 1
48
+ end
49
+
50
+ 0
51
+ end
52
+
53
+ private
54
+
55
+ def remove_shell_colors(string)
56
+ string.gsub(/\e\[(\d+)(;\d+)*m/, "")
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class Rubofix
3
+ VERSION = "0.1.0"
4
+ end
data/lib/rubofix.rb ADDED
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ class Rubofix
8
+ def initialize(url:, api_key:, model:, context:)
9
+ @url = url
10
+ @api_key = api_key
11
+ @model = model
12
+ @context = context
13
+ end
14
+
15
+ def fix!(offense)
16
+ # parse offense
17
+ match = offense.match(/(.+):(\d+):\d+: \S+ (\S+?):/)
18
+ raise "unable to parse offense #{offense}" unless match
19
+ path = match[1]
20
+ line_number = Integer(match[2])
21
+ offense_name = match[3]
22
+ line, context = lines_from_file(path, line_number)
23
+
24
+ # ask openai for a fix
25
+ prompt =
26
+ case offense_name
27
+ when "Gemspec/DevelopmentDependencies"
28
+ <<~PROMPT
29
+ Act as ruby code formatter, convert this line taken from a gemspec file into a `gem` method call with `group: :development` for a Gemfile.
30
+ Never changes meaning, just formatting.
31
+ - Print only the fixed line, NOTHING ELSE
32
+ - keep ONLY EXISTING comments
33
+ - DO NOT ADD NEW COMMENTS
34
+ - remove leading whitespace
35
+ #{line}
36
+ PROMPT
37
+ else
38
+ <<~PROMPT
39
+ Act as ruby code formatter, that never changes meaning, just formatting.
40
+ This is important production code, nothing except formatting should be changed.
41
+
42
+ Fix this rubocop offense: #{offense}
43
+ - Print only the fixed line, NOTHING ELSE
44
+ - Do not change the meaning or intent of the code
45
+ The CONTEXT is as follows:
46
+ #{context.join("\n")}
47
+ PROMPT
48
+ end
49
+ puts "prompt:#{prompt}" if ENV["DEBUG"]
50
+ answer = send_to_openai(prompt)
51
+ puts "answer:\n#{answer}" if ENV["DEBUG"]
52
+ puts "Fixing #{offense} with:\n#{answer}"
53
+
54
+ answer = answer.strip.sub(/\A```ruby\n(.*)\n```\z/m, "\\1") # it always adds these even when asked to not add
55
+
56
+ case offense_name
57
+ when "Gemspec/DevelopmentDependencies"
58
+ remove_line_in_file(path, line_number)
59
+ append_line_to_file("Gemfile", answer)
60
+ else
61
+ # replace line in file
62
+ whitespace = line[/\A\s*/]
63
+ answer = "#{whitespace}#{answer.lstrip}" # it often gets confused and messes up the whitespace
64
+ replace_line_in_file(path, line_number, answer)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def lines_from_file(file_path, line_number)
71
+ start_line = [line_number - @context, 1].max
72
+ end_line = line_number + @context
73
+
74
+ lines = File.read(file_path).split("\n", -1)
75
+ context = lines.each_with_index.map { |l, i| "line #{(i + 1).to_s.rjust(5, " ")}:#{l}" }
76
+ [lines[line_number - 1], context[start_line - 1..end_line - 1]]
77
+ end
78
+
79
+ def replace_line_in_file(file_path, line_number, new_line)
80
+ lines = File.read(file_path).split("\n", -1)
81
+ lines[line_number - 1] = new_line
82
+ File.write(file_path, lines.join("\n"))
83
+ end
84
+
85
+ def append_line_to_file(path, answer)
86
+ File.open(path, "a") do |f|
87
+ f.puts(answer)
88
+ end
89
+ end
90
+
91
+ def remove_line_in_file(path, line_number)
92
+ lines = File.read(path).split("\n", -1)
93
+ lines.delete_at(line_number - 1)
94
+ File.write(path, lines.join("\n"))
95
+ end
96
+
97
+ def send_to_openai(prompt)
98
+ uri = URI.parse("#{@url}/v1/chat/completions")
99
+ request = Net::HTTP::Post.new(uri)
100
+ request.content_type = "application/json"
101
+ request["Authorization"] = "Bearer #{@api_key}"
102
+
103
+ request.body = JSON.dump(
104
+ {
105
+ model: @model,
106
+ messages: [{ role: "user", content: prompt }],
107
+ max_tokens: 150
108
+ }
109
+ )
110
+
111
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
112
+ http.request(request)
113
+ end
114
+ raise "Invalid http response #{response.code}:\n#{response.body}" unless response.is_a?(Net::HTTPSuccess)
115
+
116
+ JSON.parse(response.body)["choices"][0]["message"]["content"]
117
+ end
118
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubofix
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-09-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: michael@grosser.it
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - MIT-LICENSE
20
+ - bin/rubofix
21
+ - lib/rubofix.rb
22
+ - lib/rubofix/cli.rb
23
+ - lib/rubofix/version.rb
24
+ homepage: https://github.com/grosser/rubofix
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.1.0
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.4.10
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: Auto fix all rubocop warnings with chatgpt / openai / local llm
47
+ test_files: []