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 +7 -0
- data/MIT-LICENSE +20 -0
- data/bin/rubofix +11 -0
- data/lib/rubofix/cli.rb +59 -0
- data/lib/rubofix/version.rb +4 -0
- data/lib/rubofix.rb +118 -0
- metadata +47 -0
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)
|
data/lib/rubofix/cli.rb
ADDED
@@ -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
|
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: []
|