rubofix 0.1.0

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