Module: Mbeditor::RubocopRunService

Defined in:
app/services/mbeditor/rubocop_run_service.rb

Overview

Whole-workspace rubocop run — the counterpart to the per-buffer /lint endpoint, which can only report on files Monaco already has open.

-a (safe autocorrect) rather than -A: unsafe corrections can change behaviour, and this runs over every file in the project at once, so an unreviewed -A is a much bigger blast radius than the per-buffer quick fix.

Class Method Summary collapse

Class Method Details

.error(message) ⇒ Object



82
83
84
# File 'app/services/mbeditor/rubocop_run_service.rb', line 82

def error(message)
  { ok: false, error: message, files: [], summary: {}, correctable: 0 }
end

.offense(o) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
# File 'app/services/mbeditor/rubocop_run_service.rb', line 70

def offense(o)
  {
    copName: o["cop_name"],
    message: o["message"],
    severity: o["severity"],
    correctable: o["correctable"] == true,
    corrected: o["corrected"] == true,
    line: o.dig("location", "start_line") || o.dig("location", "line") || 1,
    column: o.dig("location", "start_column") || o.dig("location", "column") || 1
  }
end

.parse(stdout, stderr = nil, matcher = nil) ⇒ Object

RuboCop writes its JSON to stdout, but anything the host app prints while loading (a deprecation warning, a bundler notice) lands in front of it — hence the seek to the first brace, same as the /lint path. matcher drops offenses in paths the workspace excludes — vendored gems above all. RuboCop's own defaults exclude vendor/**/*, but a host .rubocop.yml that sets AllCops/Exclude without inherit_mode: merge silently replaces them, and the panel then fills with gem source.

ponytail: filtered after the fact, so a host in that state still pays for the walk. Generate a config that inherits from theirs and re-adds the excludes if the run time ever becomes the complaint.



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'app/services/mbeditor/rubocop_run_service.rb', line 46

def parse(stdout, stderr = nil, matcher = nil)
  idx = stdout.index("{")
  return error(stderr.to_s.strip.split("\n").last || "RuboCop produced no output") unless idx

  data = JSON.parse(stdout[idx..])
  files = (data["files"] || []).filter_map do |file|
    next if matcher&.excluded?(file["path"].to_s)

    offenses = file["offenses"] || []
    next if offenses.empty?

    { path: file["path"], offenses: offenses.map { |o| offense(o) } }
  end

  {
    ok: true,
    files: files.sort_by { |f| f[:path].to_s },
    summary: data["summary"] || {},
    correctable: files.sum { |f| f[:offenses].count { |o| o[:correctable] } }
  }
rescue JSON::ParserError => e
  error(e.message)
end

.run(workspace_root, mode: :check, timeout: 300) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'app/services/mbeditor/rubocop_run_service.rb', line 16

def run(workspace_root, mode: :check, timeout: 300)
  matcher = ExclusionMatcher.new(Mbeditor.configuration.excluded_paths, root: workspace_root)
  cmd = AvailabilityProbe.rubocop_command(workspace_root) +
        [AvailabilityProbe.rubocop_server_flag(workspace_root), "--format", "json", "--no-color"]
  cmd << "-a" if mode.to_sym == :autocorrect

  result = ProcessRunner.call(
    cmd,
    timeout: timeout,
    chdir: workspace_root.to_s,
    env: { "RUBOCOP_CACHE_ROOT" => File.join(Dir.tmpdir, "rubocop") }
  )
  parse(result[:stdout], result[:stderr], matcher)
rescue ProcessRunner::TimeoutError
  error("RuboCop timed out after #{timeout}s")
rescue StandardError => e
  error(e.message)
end