Class: Ace::Lint::Molecules::RubyLinter

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/lint/molecules/ruby_linter.rb

Overview

Lints Ruby files using StandardRB (preferred) with RuboCop fallback Supports multi-validator mode via ValidatorChain

Class Method Summary collapse

Class Method Details

.build_offense_message(offense) ⇒ String

Build formatted offense message

Parameters:

  • offense (Hash)

    Offense data

Returns:

  • (String)

    Formatted message



190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/ace/lint/molecules/ruby_linter.rb', line 190

def self.build_offense_message(offense)
  file = offense[:file]
  line = offense[:line]
  column = offense[:column]
  message = offense[:message]

  if file && line && line > 0
    "#{file}:#{line}:#{column}: #{message}"
  else
    message
  end
end

.convert_to_validation_errors(offenses) ⇒ Array<Models::ValidationError>

Convert offense hash to ValidationError

Parameters:

  • offenses (Array<Hash>)

    Offense data

Returns:



162
163
164
165
166
167
168
169
# File 'lib/ace/lint/molecules/ruby_linter.rb', line 162

def self.convert_to_validation_errors(offenses)
  return [] if offenses.nil? || offenses.empty?

  offenses.map do |offense|
    message = build_offense_message(offense)
    Models::ValidationError.new(message: message)
  end
end

.lint(file_path, options: {}) ⇒ Models::LintResult

Lint a Ruby file

Parameters:

  • file_path (String)

    Path to the Ruby file

  • options (Hash) (defaults to: {})

    Linting options

Options Hash (options:):

  • :fix (Boolean)

    Apply autofix

  • :validators (Array<Symbol>)

    Specific validators to use

  • :fallback_validators (Array<Symbol>)

    Fallback validators

Returns:



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/ace/lint/molecules/ruby_linter.rb', line 22

def self.lint(file_path, options: {})
  fix = options[:fix] || false
  validators = options[:validators]
  fallback_validators = options[:fallback_validators]

  # Read file content before running validators to detect actual changes
  before_content = fix ? File.read(file_path) : nil

  result, runner = run_validators([file_path], fix: fix, validators: validators,
    fallback_validators: fallback_validators)

  # Determine success by error count, not runner exit status
  # (runner exits non-zero for convention warnings too)
  file_errors = result[:errors] || []
  file_warnings = result[:warnings] || []

  # Read file once after validation to detect formatting changes
  after_content = fix ? File.read(file_path) : nil

  Models::LintResult.new(
    file_path: file_path,
    success: file_errors.empty?,
    errors: convert_to_validation_errors(file_errors),
    warnings: convert_to_validation_errors(file_warnings),
    formatted: fix && before_content != after_content,
    runner: runner
  )
rescue => e
  Models::LintResult.new(
    file_path: file_path,
    success: false,
    errors: [Models::ValidationError.new(message: "Ruby linting failed: #{e.message}")],
    warnings: [],
    formatted: false,
    runner: nil
  )
end

.lint_batch(file_paths, options: {}) ⇒ Array<Models::LintResult>

Lint multiple Ruby files in a single subprocess Supports multiple validators via ValidatorChain

Parameters:

  • file_paths (Array<String>)

    Paths to Ruby files

  • options (Hash) (defaults to: {})

    Linting options

Options Hash (options:):

  • :fix (Boolean)

    Apply autofix

  • :validators (Array<Symbol>)

    Specific validators to use

  • :fallback_validators (Array<Symbol>)

    Fallback validators

  • :quiet (Boolean)

    Suppress chain warnings (default: false)

Returns:



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/ace/lint/molecules/ruby_linter.rb', line 69

def self.lint_batch(file_paths, options: {})
  return [] if file_paths.empty?

  fix = options[:fix] || false
  quiet = options[:quiet] || false
  validators = options[:validators]
  fallback_validators = options[:fallback_validators]

  # Read file contents before running validators to detect actual changes
  before_contents = fix ? file_paths.to_h { |fp| [fp, File.read(fp)] } : {}

  result, runner = run_validators(file_paths, fix: fix, validators: validators,
    fallback_validators: fallback_validators)

  # Surface chain-level warnings (e.g., unavailable validators) unless quiet
  unless quiet
    chain_warnings = result[:chain_warnings] || []
    chain_warnings.each do |warning|
      warn "[ace-lint] #{warning}"
    end
  end

  # Group offenses by file
  offenses_by_file = Hash.new { |h, k| h[k] = {errors: [], warnings: []} }

  result[:errors].each do |offense|
    offenses_by_file[offense[:file] || :_general_][:errors] << offense
  end

  result[:warnings].each do |offense|
    offenses_by_file[offense[:file] || :_general_][:warnings] << offense
  end

  # Check for runner-level failure (errors without file context)
  # If all errors are general (no :file key), the runner itself failed
  general_errors = offenses_by_file[:_general_]&.dig(:errors) || []
  file_specific_errors_exist = offenses_by_file.keys.any? { |k| k != :_general_ }

  if !result[:success] && general_errors.any? && !file_specific_errors_exist
    # Runner failed with no file-specific offenses - propagate to all files
    error_msg = general_errors.first&.dig(:message) || "StandardRB execution failed"
    return file_paths.map do |file_path|
      Models::LintResult.new(
        file_path: file_path,
        success: false,
        errors: [Models::ValidationError.new(message: error_msg)],
        warnings: [],
        formatted: false,
        runner: runner
      )
    end
  end

  # Read files once after validation to detect formatting changes
  after_contents = fix ? file_paths.to_h { |fp| [fp, File.read(fp)] } : {}

  # Create a LintResult for each file
  file_paths.map do |file_path|
    offenses = offenses_by_file[file_path]
    general = offenses_by_file[:_general_]

    # Include general errors (without file context) for each file
    file_errors = (offenses ? offenses[:errors] : []) + (general ? general[:errors] : [])
    file_warnings = (offenses ? offenses[:warnings] : []) + (general ? general[:warnings] : [])

    Models::LintResult.new(
      file_path: file_path,
      success: file_errors.empty?,
      errors: convert_to_validation_errors(file_errors),
      warnings: convert_to_validation_errors(file_warnings),
      formatted: fix && before_contents[file_path] != after_contents[file_path],
      runner: runner
    )
  end
rescue => e
  # If batch fails, return individual error results for each file
  # Include error context for debugging
  warn "Ruby batch linting failed: #{e.message}" if $VERBOSE
  file_paths.map do |file_path|
    Models::LintResult.new(
      file_path: file_path,
      success: false,
      errors: [Models::ValidationError.new(message: "Batch linting failed: #{e.message}")],
      warnings: [],
      formatted: false,
      runner: nil
    )
  end
end