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



94
95
96
# File 'app/services/mbeditor/rubocop_run_service.rb', line 94

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

.offense(o) ⇒ Object



83
84
85
86
87
88
89
90
91
92
# File 'app/services/mbeditor/rubocop_run_service.rb', line 83

def offense(o)
  {
    copName: o["cop_name"],
    message: o["message"],
    severity: o["severity"],
    correctable: o["correctable"] == 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, mode: :check) ⇒ 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.

-a reports what it fixed alongside what it left: a corrected offense still appears in the JSON, with corrected: true and correctable: true. Rendering those made the Fix button look inert — the list after a run was byte-identical to the list before it, and the correctable tally never moved, so the button stayed lit and every further click was a slow no-op. They are dropped here rather than at the panel because /rubocop's response is documented as the post-correction offense list.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'app/services/mbeditor/rubocop_run_service.rb', line 54

def parse(stdout, stderr = nil, matcher = nil, mode: :check)
  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"] || []).reject { |o| o["corrected"] == true }
    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"] || {},
    # RuboCop's `correctable` means "a cop could rewrite this", safely or
    # not. `-a` already looped to convergence, so whatever survived it needs
    # `-A` — which this whole-workspace path deliberately never runs. Left
    # as the raw tally, the Fix button never disabled: Style/
    # FrozenStringLiteralComment and friends are correctable forever.
    correctable: mode.to_sym == :autocorrect ? 0 : 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, mode: mode)
rescue ProcessRunner::TimeoutError
  error("RuboCop timed out after #{timeout}s")
rescue StandardError => e
  error(e.message)
end