Module: Snoot::CLI

Defined in:
lib/snoot/cli.rb,
lib/snoot/cli/event.rb,
lib/snoot/cli/pipeline.rb

Overview

The CLI surface. cli.rb owns the surface entry points: argv-shape entry (.run – UsageErrorExit), in-process entry (.run_invoked), the surface constants (banners, exit codes, the default path set), and the IO emitter helpers (emit_warnings, emit_failure, emit_nothing_to_report, format_report). The event values (RunInvoked, ReportEmitted) live in cli/event.rb; the Pipeline value lives in cli/pipeline.rb. .run consults BANNERS for –version, –help, and -h, rejects unknown flags with exit 64, and otherwise threads argv through .run_invoked, mapping the terminal outcome to an EXIT_CODES integer.

The two entry points return deliberately different shapes. .run is the POSIX boundary: argv -> Integer, consumed by ‘exit Snoot::CLI.run(ARGV)` in exe/snoot. .run_invoked is the in-process boundary: Set<Path> -> [Run, Array of event values], used by tests and any library embedding that needs the full event list. Internally .run calls .run_invoked and projects Run#outcome through EXIT_CODES, so the integer is a lossy view of the Run; callers that need the Run, the events, or both must use .run_invoked.

Defined Under Namespace

Classes: Pipeline, ReportEmitted, RunInvoked

Constant Summary collapse

NOTHING_TO_REPORT =
"nothing to report -- no findings above snoot's significance floor\n"
DEFAULT_PATHS =
Set[Snoot::Path.new(raw: ".")].freeze
USAGE =
<<~HELP
  snoot - single-finding reek/flog/flay reporter

  Usage: snoot [paths...]
         snoot --version
         snoot --help

  With no path arguments, snoot scans the current directory.

  Exit codes:
    0   nothing to report
    1   one finding rendered
    2   analyser failure
    64  usage error

  Example:
    snoot lib/
HELP
EXIT_CODES =
{
  finding_rendered: 1,
  nothing_to_report: 0,
  analysis_failed: 2,
  usage_error: 64
}.freeze
BANNERS =
{
  %w[--version] => "snoot #{Snoot::VERSION}\n",
  %w[-h --help] => USAGE
}.freeze

Class Method Summary collapse

Class Method Details

.build_paths(argv) ⇒ Object



113
114
115
# File 'lib/snoot/cli.rb', line 113

def build_paths(argv)
  argv.each_with_object(Set[]) { |raw, set| set << Path.new(raw: raw) }
end

.collect_events(paths, pipeline) ⇒ Object



122
123
124
125
126
127
128
129
# File 'lib/snoot/cli.rb', line 122

def collect_events(paths, pipeline)
  AnalyseRun.invoke(paths, orchestration: pipeline.orchestration) =>
    { run:, events: analyse_events, smells: }
  emit_warnings(analyse_events, pipeline.stderr)
  events = [RunInvoked.new(paths: paths), *analyse_events,
            *events_for_outcome(run, smells, pipeline: pipeline)]
  [run, events]
end

.emit_failure(run, stderr) ⇒ Object



67
68
69
70
71
# File 'lib/snoot/cli.rb', line 67

def emit_failure(run, stderr)
  failure = run.failure
  stderr.write("analysis failed (#{failure.analyser}): #{failure.message}\n")
  []
end

.emit_nothing_to_report(stdout) ⇒ Object



62
63
64
65
# File 'lib/snoot/cli.rb', line 62

def emit_nothing_to_report(stdout)
  stdout.write(NOTHING_TO_REPORT)
  []
end

.emit_report(run, smells, pipeline:) ⇒ Object



140
141
142
143
144
145
# File 'lib/snoot/cli.rb', line 140

def emit_report(run, smells, pipeline:)
  RenderReport.invoke(run, smells: smells, orchestration: pipeline.orchestration) =>
    { sections:, finding: }
  pipeline.stdout.write(format_report(sections))
  ReportEmitted.new(run: run, finding: finding, sections: sections)
end

.emit_warnings(analyse_events, stderr) ⇒ Object



73
74
75
76
77
78
79
# File 'lib/snoot/cli.rb', line 73

def emit_warnings(analyse_events, stderr)
  analyse_events.each do |event|
    next unless event.is_a?(AnalyseRun::SkippedDocLessSmellWarned)

    stderr.write("warning: skipping doc-less smell type '#{event.smell_type.name}'\n")
  end
end

.events_for_outcome(run, smells, pipeline:) ⇒ Object



131
132
133
134
135
136
137
138
# File 'lib/snoot/cli.rb', line 131

def events_for_outcome(run, smells, pipeline:)
  case run.outcome
  when :finding_rendered then [emit_report(run, smells, pipeline: pipeline)]
  when :nothing_to_report then emit_nothing_to_report(pipeline.stdout)
  when :analysis_failed then emit_failure(run, pipeline.stderr)
  else []
  end
end

.format_report(sections) ⇒ Object



81
82
83
# File 'lib/snoot/cli.rb', line 81

def format_report(sections)
  "#{sections.values.join("\n\n")}\n"
end

.lookup_banner(argv) ⇒ Object



93
94
95
96
97
# File 'lib/snoot/cli.rb', line 93

def lookup_banner(argv)
  return nil unless argv.length == 1

  BANNERS.find { |flags, _| flags.include?(argv.first) }&.last
end

.run(argv, pipeline: Pipeline.default) ⇒ Object



85
86
87
88
89
90
91
# File 'lib/snoot/cli.rb', line 85

def run(argv, pipeline: Pipeline.default)
  banner = lookup_banner(argv)
  return write_and_return(pipeline.stdout, banner, 0) if banner
  return write_and_return(pipeline.stderr, USAGE, EXIT_CODES.fetch(:usage_error)) if unknown_flag?(argv)

  run_pipeline(argv, pipeline: pipeline)
end

.run_invoked(paths, pipeline: Pipeline.default) ⇒ Object



117
118
119
120
# File 'lib/snoot/cli.rb', line 117

def run_invoked(paths, pipeline: Pipeline.default)
  paths = DEFAULT_PATHS if paths.empty?
  collect_events(paths, pipeline)
end

.run_pipeline(argv, pipeline:) ⇒ Object



108
109
110
111
# File 'lib/snoot/cli.rb', line 108

def run_pipeline(argv, pipeline:)
  run, _events = run_invoked(build_paths(argv), pipeline: pipeline)
  EXIT_CODES.fetch(run.outcome)
end

.unknown_flag?(argv) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/snoot/cli.rb', line 104

def unknown_flag?(argv)
  argv.any? { |arg| arg.start_with?("-") }
end

.write_and_return(io, message, code) ⇒ Object



99
100
101
102
# File 'lib/snoot/cli.rb', line 99

def write_and_return(io, message, code)
  io.write(message)
  code
end