Module: RailVerdict::Analyzers::Shared

Defined in:
lib/rail_verdict/analyzers/_shared.rb

Constant Summary collapse

FALLBACK_MESSAGE_SUFFIX =

Canonical finding message normalization — single path for all analyzers. Guarantees deterministic, bounded, valid non-empty UTF-8.

"reported a finding without a message"

Class Method Summary collapse

Class Method Details

.bounded_message(message) ⇒ Object



8
9
10
# File 'lib/rail_verdict/analyzers/_shared.rb', line 8

def bounded_message(message)
  message.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")[0, 4096]
end

.canonical_tool_version(version) ⇒ Object



71
72
73
74
# File 'lib/rail_verdict/analyzers/_shared.rb', line 71

def canonical_tool_version(version)
  normalized = normalize_tool_version(version)
  normalized.nil? || normalized.empty? ? "unknown" : normalized
end

.detail_for(run_result) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/rail_verdict/analyzers/_shared.rb', line 12

def detail_for(run_result)
  detail = run_result.detail.to_s
  stderr = run_result.stderr.to_s.lines.first.to_s.strip
  combined = [detail, stderr].reject(&:empty?).join(": ")
  if combined.empty?
    combined = if run_result.status == :exited
      "analyzer exited with status #{run_result.exit_code} and produced no diagnostic output"
    else
      "analyzer produced no diagnostic output"
    end
  end
  bounded_message(combined)
end

.execution_message(run_result) ⇒ Object



26
27
28
29
30
31
# File 'lib/rail_verdict/analyzers/_shared.rb', line 26

def execution_message(run_result)
  message = run_result.stderr.to_s.lines.first.to_s.strip
  message = "#{run_result.detail}" if message.empty?
  message = "execution failed with status #{run_result.exit_code}" if message.empty?
  bounded_message(message)
end

.failure_result(analyzer_id:, invocation:, status:, message:, tool_version: nil) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/rail_verdict/analyzers/_shared.rb', line 49

def failure_result(analyzer_id:, invocation:, status:, message:, tool_version: nil)
  status = status.to_s
  bounded = bounded_message(message.to_s)
  bounded = "#{status}: analyzer produced no diagnostic output" if bounded.strip.empty?
  normalized_version = normalize_tool_version(tool_version)
  AnalyzerResult.new(
    analyzer: analyzer_id,
    tool_version: normalized_version,
    invocation: invocation,
    execution_status: status,
    finding_ids: [],
    failure: { "code" => status, "message" => bounded }
  )
end

.invocation_for(command, arguments) ⇒ Object



42
43
44
45
46
47
# File 'lib/rail_verdict/analyzers/_shared.rb', line 42

def invocation_for(command, arguments)
  {
    "executable" => command.fetch(:executable),
    "argv" => command.fetch(:args_prefix).dup.concat(arguments)
  }
end

.normalize_finding_message(analyzer_id, raw_message) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rail_verdict/analyzers/_shared.rb', line 80

def normalize_finding_message(analyzer_id, raw_message)
  raw = raw_message.to_s.dup
  # Handle invalid UTF-8, null bytes
  raw = raw.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD")
  raw = raw.scrub("\uFFFD")
  raw = raw.delete("\u0000")
  # Strip ANSI escape sequences
  raw = raw.gsub(/\e\[[0-9;]*[A-Za-z]/, "")
  raw = raw.gsub(/\e\][^\a]*\a/, "")
  raw = raw.gsub(/\e\(B/, "")
  # Remove control characters except tab/newline then normalize whitespace
  raw = raw.gsub(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/, "")
  raw = raw.strip
  # Collapse whitespace including tabs/newlines
  raw = raw.gsub(/\s+/, " ").strip
  if raw.empty?
    "#{analyzer_id} #{FALLBACK_MESSAGE_SUFFIX}"
  else
    raw = raw.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD").scrub("\uFFFD")
    raw.bytesize > 4096 ? raw.byteslice(0, 4096).scrub("\uFFFD").strip : raw
  end
end

.normalize_tool_version(version) ⇒ Object



64
65
66
67
68
69
# File 'lib/rail_verdict/analyzers/_shared.rb', line 64

def normalize_tool_version(version)
  return nil if version.nil?
  str = version.to_s.strip
  return nil if str.empty?
  str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?").scrub("?")[0, 128]
end

.parse_semver(output) ⇒ Object



37
38
39
40
# File 'lib/rail_verdict/analyzers/_shared.rb', line 37

def parse_semver(output)
  match = output.to_s.match(/\b(\d+\.\d+\.\d+)\b/)
  match && match[1]
end

.truncated?(run_result) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
# File 'lib/rail_verdict/analyzers/_shared.rb', line 33

def truncated?(run_result)
  run_result.stdout_truncated || run_result.stderr_truncated
end