Class: StudFinder::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/stud_finder/cli.rb

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: Analysis, Report, ValidationError

Constant Summary collapse

OUTPUT_FORMATS =
%w[table json markdown csv].freeze
CLASS_RANK =
{ 'trunk' => 3, 'branch' => 2, 'leaf' => 1 }.freeze
RESULT_COLUMNS =
%w[
  rank language file score evidence class new_file age_days escalation fan_in fan_in_pct fan_out fan_out_pct
  instability instability_pct complexity complexity_pct churn_commits churn_lines churn_pct loc loc_pct max_coupling
  max_coupling_partner coupling_partners coupling_pct coverage
].freeze
MARKDOWN_COLUMNS =
%w[
  rank language file score evidence class new_file age_days escalation fan_in fan_out fan_out_pct instability
  complexity
  churn_commits churn_lines churn_pct max_coupling max_coupling_partner coupling_partners coupling_pct coverage
].freeze
WEIGHT_KEYS =
%i[fan_in fan_out complexity churn coverage].freeze
OPTIONAL_WEIGHT_KEYS =
%i[interaction coupling].freeze
DEFAULT_OPTIONS =
{
  output: 'table',
  churn_days: 180,
  weights: StudFinder::Scorer::DEFAULT_WEIGHTS,
  custom_weights: false,
  trunk_threshold: 85,
  branch_threshold: 50,
  excludes: [],
  min_files: 20,
  top: nil,
  verbose: false,
  ruby_coverage_path: nil,
  js_coverage_path: nil,
  js_timeout: 60,
  rails_inference: true,
  diff_base: nil,
  only_paths: nil,
  filter_set: nil,
  coupling_threshold: 0.30,
  coupling_min_commits: 5,
  coupling_max_commit_files: 50,
  newness: true,
  new_file_days: StudFinder::Newness::DEFAULT_DAYS,
  new_file_min_commits: StudFinder::Newness::DEFAULT_MIN_COMMITS,
  auto_unshallow: true,
  cli_warnings: []
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argv, stdout: $stdout, stderr: $stderr, stdin: $stdin) ⇒ CLI

Returns a new instance of CLI.



78
79
80
81
82
83
84
# File 'lib/stud_finder/cli.rb', line 78

def initialize(argv, stdout: $stdout, stderr: $stderr, stdin: $stdin)
  @argv = argv.dup
  @stdout = stdout
  @stderr = stderr
  @stdin = stdin
  @options = Marshal.load(Marshal.dump(DEFAULT_OPTIONS))
end

Class Method Details

.start(argv = ARGV, stdout: $stdout, stderr: $stderr, stdin: $stdin) ⇒ Object



86
87
88
# File 'lib/stud_finder/cli.rb', line 86

def self.start(argv = ARGV, stdout: $stdout, stderr: $stderr, stdin: $stdin)
  new(argv, stdout: stdout, stderr: stderr, stdin: stdin).run
end

Instance Method Details

#gate_input(input_path) ⇒ Object



162
163
164
165
166
167
168
169
# File 'lib/stud_finder/cli.rb', line 162

def gate_input(input_path)
  return File.read(input_path) if input_path
  return @stdin.read if !@stdin.respond_to?(:tty?) || !@stdin.tty?

  raise ValidationError, 'Error: provide --input FILE or pipe JSON to stdin.'
rescue Errno::ENOENT
  raise ValidationError, "Error: input file not found: #{input_path}"
end

#runObject



90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/stud_finder/cli.rb', line 90

def run
  parser = option_parser
  return run_gate if shift_subcommand?('gate')
  return run_edges_subcommand(parser) if shift_subcommand?('edges')

  run_scan(parser)
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::InvalidArgument, ValidationError,
       FileCollector::Error, Gate::Error, Churn::Error, Complexity::Error, Coverage::Cobertura::Error,
       Coverage::Detector::Error, Coverage::Lcov::Error, Coverage::Resultset::Error, Diff::Error, Newness::Error,
       Scorer::ValidationError => e
  @stderr.puts e.message
  1
end

#run_edges(target, path) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/stud_finder/cli.rb', line 171

def run_edges(target, path)
  @repo_path = File.expand_path(path)
  result = FileCollector.new(path: path, excludes: @options[:excludes],
                             min_files: @options[:min_files], stderr: @stderr).collect
  progress("collecting files... #{result.files.length} found")
  analysis = analyze(@repo_path, result.files, result.languages)
  all_rows = analysis.ruby.rows + analysis.javascript.rows
  all_edges = analysis.ruby.edges.merge(analysis.javascript.edges)

  progress("computing temporal coupling (git log, #{@options[:churn_days]} days)...")
  coupling_result = TemporalCoupling.new(
    repo_path: @repo_path,
    files: result.files,
    days: @options[:churn_days],
    min_co_changes: @options[:coupling_min_commits],
    coupling_threshold: @options[:coupling_threshold],
    max_commit_files: @options[:coupling_max_commit_files]
  ).call

  Edges.new(
    target: target, rows: all_rows, edges: all_edges,
    coupling: coupling_result.pairs,
    churn_days: @options[:churn_days],
    coupling_min_commits: @options[:coupling_min_commits],
    coupling_threshold: @options[:coupling_threshold],
    stdout: @stdout, stderr: @stderr
  ).call
rescue FileCollector::Error, Churn::Error, Complexity::Error, Newness::Error, Scorer::ValidationError => e
  @stderr.puts e.message
  1
end

#run_edges_subcommand(parser) ⇒ Object

Raises:



136
137
138
139
140
141
142
143
144
145
# File 'lib/stud_finder/cli.rb', line 136

def run_edges_subcommand(parser)
  parser.parse!(@argv)
  target = @argv.shift
  path = @argv.shift || '.'
  raise ValidationError, "Error: unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?

  @repo_path = File.expand_path(path)
  validate_options!
  run_edges(target, path)
end

#run_gateObject

Raises:



147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/stud_finder/cli.rb', line 147

def run_gate
  gate_options = { input: nil, enforce: false }
  OptionParser.new do |opts|
    opts.banner = 'Usage: stud-finder gate [--input FILE] [--enforce]'
    opts.on('--input FILE', 'Read stud-finder JSON output from FILE') { |value| gate_options[:input] = value }
    opts.on('--enforce', 'Exit non-zero when gate findings are present') { gate_options[:enforce] = true }
  end.parse!(@argv)
  raise ValidationError, "Error: unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?

  json = gate_input(gate_options[:input])
  result = Gate.call(json)
  @stdout.puts Gate.markdown(result, enforce: gate_options[:enforce])
  gate_options[:enforce] && result.findings? ? 1 : 0
end

#run_scan(parser) ⇒ Object

Raises:



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/stud_finder/cli.rb', line 111

def run_scan(parser)
  parser.parse!(@argv)
  path = @argv.shift || '.'
  raise ValidationError, "Error: unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?

  @repo_path = File.expand_path(path)
  validate_options!

  result = FileCollector.new(
    path: path,
    excludes: @options[:excludes],
    min_files: @options[:min_files],
    stderr: @stderr
  ).collect
  progress("collecting files... #{result.files.length} found")

  @options[:filter_set] = resolve_filter_set(@repo_path)

  coupling = compute_coupling(@repo_path, result.files)
  analysis = analyze(@repo_path, result.files, result.languages, coupling)
  analysis = warn_if_no_scored_files(analysis)
  emit_results(@repo_path, result, analysis)
  0
end

#shift_subcommand?(name) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
107
108
109
# File 'lib/stud_finder/cli.rb', line 104

def shift_subcommand?(name)
  return false unless @argv[0] == name

  @argv.shift
  true
end