Class: Reviewer::Runner

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/reviewer/runner.rb,
lib/reviewer/runner/result.rb,
lib/reviewer/runner/guidance.rb,
lib/reviewer/runner/formatter.rb,
lib/reviewer/runner/failed_files.rb,
lib/reviewer/runner/strategies/captured.rb,
lib/reviewer/runner/strategies/passthrough.rb

Overview

Wrapper for executng a command and printing the results

Defined Under Namespace

Modules: Strategies Classes: FailedFiles, Formatter, Guidance, Result

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tool, command_type, strategy = Strategies::Captured, context:) ⇒ self

Creates a wrapper for running commands through Reviewer in order to provide a more accessible

API for recording execution time and interpreting the results of a command in a more
generous way so that non-zero exit statuses can still potentially be passing.

Parameters:

  • tool (Symbol)

    the key for the desired tool to run

  • command_type (Symbol)

    the key for the type of command to run

  • strategy (Runner::Strategies) (defaults to: Strategies::Captured)

    how to execute and handle the results of the command

  • context (Context)

    the shared runtime dependencies (arguments, output, history)



34
35
36
37
38
39
40
41
# File 'lib/reviewer/runner.rb', line 34

def initialize(tool, command_type, strategy = Strategies::Captured, context:)
  @command = Command.new(tool, command_type, context: context)
  @strategy = strategy
  @shell = Shell.new(stream: context.output.printer.stream)
  @context = context
  @skipped = false
  @missing = false
end

Instance Attribute Details

#commandObject (readonly)

Returns the value of attribute command.



19
20
21
# File 'lib/reviewer/runner.rb', line 19

def command
  @command
end

#command_stringString (readonly)

Returns the full command string that was executed.

Returns:

  • (String)

    the full command string that was executed



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#command_typeSymbol (readonly)

Returns the type of command run (:review, :format, etc.).

Returns:

  • (Symbol)

    the type of command run (:review, :format, etc.)



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#durationFloat (readonly)

Returns the execution time in seconds.

Returns:

  • (Float)

    the execution time in seconds



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#exit_statusInteger (readonly)

Returns the exit status code from the command.

Returns:

  • (Integer)

    the exit status code from the command



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#missingBoolean (readonly)

Returns whether the tool’s executable was not found.

Returns:

  • (Boolean)

    whether the tool’s executable was not found



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#shellObject (readonly)

Returns the value of attribute shell.



19
20
21
# File 'lib/reviewer/runner.rb', line 19

def shell
  @shell
end

#skippedBoolean (readonly)

Returns whether the tool was skipped.

Returns:

  • (Boolean)

    whether the tool was skipped



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#stderrString? (readonly)

Returns the standard error from the command.

Returns:

  • (String, nil)

    the standard error from the command



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#stdoutString? (readonly)

Returns the standard output from the command.

Returns:

  • (String, nil)

    the standard output from the command



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#strategyClass

Returns the strategy class for running the command (Captured or Passthrough).

Returns:

  • (Class)

    the strategy class for running the command (Captured or Passthrough)



17
18
19
# File 'lib/reviewer/runner.rb', line 17

def strategy
  @strategy
end

#successBoolean (readonly)

Returns whether the command completed successfully.

Returns:

  • (Boolean)

    whether the command completed successfully



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#tool_keySymbol (readonly)

Returns the unique identifier for the tool.

Returns:

  • (Symbol)

    the unique identifier for the tool



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

#tool_nameString (readonly)

Returns the human-readable name of the tool.

Returns:

  • (String)

    the human-readable name of the tool



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
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/reviewer/runner/result.rb', line 29

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(...)
    super
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    new(
      **base_attributes(runner),
      command_string: nil,
      success: true, exit_status: 0, duration: 0,
      stdout: nil, stderr: nil, skipped: true
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      success: false, exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil, skipped: nil, missing: true
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      success: runner.success?, exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  alias_method :success?, :success
  alias_method :skipped?, :skipped
  alias_method :missing?, :missing

  # Whether this result represents a tool that actually ran (not skipped or missing)
  #
  # @return [Boolean] true if the tool was executed
  def executed? = !skipped? && !missing?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] hash representation with nil values removed
  def to_h
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing
    }.compact # Excludes summary_pattern/summary_label (config, not results)
  end
end

Instance Method Details

#execute_strategyvoid

This method returns an undefined value.

Runs the relevant strategy to either capture or pass through command output.



139
140
141
142
143
144
145
# File 'lib/reviewer/runner.rb', line 139

def execute_strategy
  # Run the provided strategy
  strategy.new(self).tap do |run_strategy|
    run_strategy.prepare if run_prepare_step?
    run_strategy.run
  end
end

#failed_filesArray<String>

Extracts file paths from stdout/stderr for failed-file tracking

Returns:

  • (Array<String>)

    file paths found in the command output



109
110
111
# File 'lib/reviewer/runner.rb', line 109

def failed_files
  FailedFiles.new(stdout, stderr).to_a
end

#failure?Boolean

Returns:

  • (Boolean)


104
# File 'lib/reviewer/runner.rb', line 104

def failure? = !success?

#formatterRunner::Formatter

Display formatter for runner-specific output (tool summary, success, failure, etc.) Computed rather than stored to avoid exceeding instance variable threshold.

Returns:



52
# File 'lib/reviewer/runner.rb', line 52

def formatter = @formatter ||= Runner::Formatter.new(output)

#guidanceGuidance

Uses the result of the runner to determine what, if any, guidance to display to help the user

get back on track in the event of an unsuccessful run.

Returns:

  • (Guidance)

    the relevant guidance based on the result of the runner



181
# File 'lib/reviewer/runner.rb', line 181

def guidance = @guidance ||= Guidance.new(command: command, result: result, context: @context)

#identify_toolvoid

This method returns an undefined value.

Prints the tool name and description to the console as a frame of reference. Only displays in streaming mode; non-streaming strategies handle their own output.



117
118
119
120
121
122
123
# File 'lib/reviewer/runner.rb', line 117

def identify_tool
  # If there's an existing result, the runner is being re-run, and identifying the tool would
  # be redundant.
  return if result.exists?

  stream_output { formatter.tool_summary(tool) }
end

#missing?Boolean

Whether this runner’s executable was not found (exit status 127)

Returns:

  • (Boolean)

    true if the tool was missing



88
# File 'lib/reviewer/runner.rb', line 88

def missing? = @missing == true

#outputOutput

The output channel for displaying content, delegated from context.

Returns:



46
# File 'lib/reviewer/runner.rb', line 46

def output = @context.output

#prepare_commandComman

Creates_an instance of the prepare command for a tool

Returns:

  • (Comman)

    the current tool’s prepare command



157
# File 'lib/reviewer/runner.rb', line 157

def prepare_command = @prepare_command ||= Command.new(tool, :prepare, context: @context)

#record_timingvoid

This method returns an undefined value.

Saves the last 5 elapsed times for the commands used this run by using the raw command as a

unique key. This enables the ability to compare times across runs while taking into
consideration that different iterations of the command may be running on fewer files. So
comparing a full run to the average time for a partial run wouldn't be helpful. By using the
raw command string, it will always be apples to apples.


172
173
174
175
# File 'lib/reviewer/runner.rb', line 172

def record_timing
  tool.record_timing(prepare_command, timer.prep)
  tool.record_timing(command, timer.main)
end

#runInteger

Executes the command and returns the exit status

Returns:

  • (Integer)

    the exit status from the command



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/reviewer/runner.rb', line 62

def run
  # Skip if files were requested but none match this tool's pattern
  if command.skip?
    @skipped = true
    show_skipped
    return 0
  end

  # Show which tool is running
  identify_tool

  # Use the provided strategy to run the command
  execute_strategy

  # Handle the result based on whether the tool was found
  result.executable_not_found? ? handle_missing : handle_result
end

#run_prepare_step?Boolean

Determines whether a preparation step should be run before the primary command. If/when the

primary command is a `:prepare` command, then it shouldn't run twice. So it skips what would
be a superfluous run of the preparation.

Returns:

  • (Boolean)

    true the primary command is not prepare and the tool needs to be prepare



152
# File 'lib/reviewer/runner.rb', line 152

def run_prepare_step? = command.type != :prepare && tool.prepare?

#show_skippedvoid

This method returns an undefined value.

Shows that a tool was skipped due to no matching files. Only displays in streaming mode; non-streaming modes report skips in the final summary.



129
130
131
132
133
134
# File 'lib/reviewer/runner.rb', line 129

def show_skipped
  stream_output do
    formatter.tool_summary(tool)
    formatter.skipped
  end
end

#skipped?Boolean

Whether this runner was skipped due to no matching files

Returns:

  • (Boolean)

    true if the tool was skipped



83
# File 'lib/reviewer/runner.rb', line 83

def skipped? = @skipped == true

#streaming?Boolean

Whether this runner is operating in streaming mode

Returns:

  • (Boolean)

    true if output should be streamed



57
# File 'lib/reviewer/runner.rb', line 57

def streaming? = @context.arguments.streaming?

#success?Boolean

Some review tools return a range of non-zero exit statuses and almost never return 0. (‘yarn audit` is a good example.) Those tools can be configured to accept a non-zero exit status so they aren’t constantly considered to be failing over minor issues.

But when other command types (prepare, install, format) are run, they either succeed or they fail. With no shades of gray in those cases, anything other than a 0 is a failure.

Skipped tools are always considered successful.

Returns:

  • (Boolean)


98
99
100
101
102
# File 'lib/reviewer/runner.rb', line 98

def success?
  return true if skipped?

  command.type == :review ? exit_status <= tool.max_exit_status : exit_status.zero?
end

#to_resultRunner::Result

Builds an immutable Result object from the current runner state

Returns:



186
187
188
# File 'lib/reviewer/runner.rb', line 186

def to_result
  Result.from_runner(self)
end

#update_last_prepared_atTime

Updates the ‘last prepared at’ timestamp that Reviewer uses to know if a tool’s preparation

step is stale and needs to be run again.

Returns:

  • (Time)

    the timestamp last_prepared_at is updated to



163
# File 'lib/reviewer/runner.rb', line 163

def update_last_prepared_at = tool.last_prepared_at = Time.now