rubofix 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|