Class: Ace::Lint::Atoms::BaseRunner
- Inherits:
-
Object
- Object
- Ace::Lint::Atoms::BaseRunner
- Defined in:
- lib/ace/lint/atoms/base_runner.rb
Overview
Base class for Ruby linter runners Provides shared parsing logic for RuboCop-style JSON output
Direct Known Subclasses
Class Method Summary collapse
-
.available? ⇒ Boolean
Check if the linter is available (cached per process, thread-safe).
-
.build_offense_item(offense, file_path = nil) ⇒ Hash
Build offense item from linter offense.
-
.command_name ⇒ String
Command name to check for availability (subclass override).
-
.parse_error_output(stdout, stderr, exit_status:) ⇒ Hash
Parse error output (issues found).
-
.parse_json_output(output, exit_status: nil) ⇒ Hash
Parse JSON output from linter (RuboCop-style format).
-
.parse_success_output(stdout) ⇒ Hash
Parse successful output (no issues).
-
.parse_text_output(output, exit_status: nil) ⇒ Hash
Parse text output (fallback for non-JSON output).
-
.reset_availability_cache! ⇒ Object
Reset availability cache for this linter (for testing purposes).
-
.run(file_paths, fix: false, config_path: nil) ⇒ Hash
Execute the linter command Subclasses must implement: tool_name, build_command, unavailable_result.
Class Method Details
.available? ⇒ Boolean
Check if the linter is available (cached per process, thread-safe)
19 20 21 22 23 24 25 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 19 def available? BaseRunner.instance_variable_get(:@availability_mutex).synchronize do cache = BaseRunner.instance_variable_get(:@availability_cache) cmd = command_name cache[cmd] ||= system_has_command?(cmd) end end |
.build_offense_item(offense, file_path = nil) ⇒ Hash
Build offense item from linter offense
193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 193 def build_offense_item(offense, file_path = nil) path = file_path || offense.dig("location", "path") || "unknown" location = offense["location"] || {} { file: path, line: location["line"] || 0, column: location["column"] || 0, message: "#{offense["cop_name"]}: #{offense["message"]}" } end |
.command_name ⇒ String
Command name to check for availability (subclass override)
38 39 40 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 38 def command_name raise NotImplementedError, "Subclass must implement command_name" end |
.parse_error_output(stdout, stderr, exit_status:) ⇒ Hash
Parse error output (issues found)
88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 88 def parse_error_output(stdout, stderr, exit_status:) unless stdout.strip.empty? begin return parse_json_output(stdout, exit_status: exit_status) rescue JSON::ParserError return parse_text_output(stderr, exit_status: exit_status) end end parse_text_output(stderr, exit_status: exit_status) end |
.parse_json_output(output, exit_status: nil) ⇒ Hash
Parse JSON output from linter (RuboCop-style format)
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 105 def parse_json_output(output, exit_status: nil) data = JSON.parse(output) errors = [] warnings = [] # RuboCop JSON format: {files: [{path, offenses: [...]}]} if data.is_a?(Hash) && data.key?("files") data["files"].each do |file_data| file_path = file_data["path"] || "unknown" offenses = file_data["offenses"] || [] offenses.each do |offense| item = build_offense_item(offense, file_path) if offense["severity"] == "error" || offense["severity"] == "fatal" errors << item else warnings << item end end end # Fallback: direct array format elsif data.is_a?(Array) data.each do |offense| item = build_offense_item(offense) if offense["severity"] == "error" || offense["severity"] == "fatal" errors << item else warnings << item end end end success = exit_status ? exit_status.zero? : errors.empty? { success: success, errors: errors, warnings: warnings } end |
.parse_success_output(stdout) ⇒ Hash
Parse successful output (no issues)
73 74 75 76 77 78 79 80 81 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 73 def parse_success_output(stdout) if stdout.strip.empty? || stdout.include?("no offenses") return {success: true, errors: [], warnings: []} end parse_json_output(stdout) rescue JSON::ParserError {success: true, errors: [], warnings: []} end |
.parse_text_output(output, exit_status: nil) ⇒ Hash
Parse text output (fallback for non-JSON output)
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 150 def parse_text_output(output, exit_status: nil) errors = [] warnings = [] output.each_line do |line| # Format: file:line:column: severity: message next unless line.match?(/^.+:\d+:\d+:/) parts = line.split(":", 5) next if parts.size < 5 item = { file: parts[0], line: parts[1].to_i, column: parts[2].to_i, message: parts[4].strip } if error_severity?(parts[3], line) errors << item else warnings << item end end success = exit_status ? exit_status.zero? : errors.empty? # Add fallback error if non-zero exit but no offenses parsed if !success && errors.empty? && warnings.empty? && !output.strip.empty? errors << {message: "#{tool_name} failed: #{output.strip.lines.first&.strip || output.strip}"} end { success: success, errors: errors, warnings: warnings } end |
.reset_availability_cache! ⇒ Object
Reset availability cache for this linter (for testing purposes)
29 30 31 32 33 34 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 29 def reset_availability_cache! BaseRunner.instance_variable_get(:@availability_mutex).synchronize do cache = BaseRunner.instance_variable_get(:@availability_cache) cache.delete(command_name) end end |
.run(file_paths, fix: false, config_path: nil) ⇒ Hash
Execute the linter command Subclasses must implement: tool_name, build_command, unavailable_result
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/ace/lint/atoms/base_runner.rb', line 48 def run(file_paths, fix: false, config_path: nil) paths = Array(file_paths) executable = resolve_command_path(command_name) return unavailable_result unless executable cmd = build_command(paths, fix: fix, config_path: config_path) cmd[0] = executable if command_invocation?(cmd[0], command_name) stdout, stderr, status = Open3.capture3(*cmd) if status.success? parse_success_output(stdout) else parse_error_output(stdout, stderr, exit_status: status.exitstatus) end rescue => e { success: false, errors: [{message: "#{tool_name} execution failed for #{Array(file_paths).join(", ")}: #{e.}"}], warnings: [] } end |