require 'optparse'
module Steep
class CLI
attr_reader :argv
attr_reader :stdout
attr_reader :stdin
attr_reader :stderr
attr_reader :command
def initialize(stdout:, stdin:, stderr:, argv:)
@stdout = stdout
@stdin = stdin
@stderr = stderr
@argv = argv
end
def self.available_commands
[:init, :check, :validate, :annotations, :version, :project, :watch, :langserver, :stats, :binstub, :checkfile]
end
def process_global_options
OptionParser.new do |opts|
opts.banner = <<~USAGE
Usage: steep [options]
available commands: #{CLI.available_commands.join(', ')}
Options:
USAGE
opts.on("--version") do
process_version
exit 0
end
handle_logging_options(opts)
end.order!(argv)
true
end
def setup_command
return false unless command = argv.shift&.to_sym
@command = command
if CLI.available_commands.include?(@command) || @command == :worker || @command == :vendor
true
else
stderr.puts "Unknown command: #{command}"
stderr.puts " available commands: #{CLI.available_commands.join(', ')}"
false
end
end
def run
process_global_options or return 1
setup_command or return 1
__send__(:"process_#{command}")
end
def handle_logging_options(opts)
opts.on("--log-level=LEVEL", "Specify log level: debug, info, warn, error, fatal") do |level|
Steep.logger.level = level
end
opts.on("--log-output=PATH", "Print logs to given path") do |file|
Steep.log_output = file
end
opts.on("--verbose", "Set log level to debug") do
Steep.logger.level = Logger::DEBUG
end
end
def handle_jobs_option(option, opts)
opts.on("-j N", "--jobs=N", "Specify the number of type check workers (defaults: #{option.default_jobs_count})") do |count|
option.jobs_count = Integer(count) if Integer(count) > 0
end
opts.on("--steep-command=COMMAND", "Specify command to exec Steep CLI for worker (defaults: steep)") do |cmd|
option.steep_command = cmd
end
end
def setup_jobs_for_ci(jobs_option)
if ENV["CI"]
unless jobs_option.jobs_count
stderr.puts Rainbow("CI environment is detected but no `--jobs` option is given.").yellow
stderr.puts " Using `[2, #{jobs_option.default_jobs_count} (# or processors)].min` to avoid hitting memory limit."
stderr.puts " Specify `--jobs` option to increase the number of jobs."
jobs_option.jobs_count = [2, jobs_option.default_jobs_count].min
end
end
end
def process_init
Drivers::Init.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep init [options]"
opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
opts.on("--force") { command.force_write = true }
handle_logging_options opts
end.parse!(argv)
end.run()
end
def process_check
Drivers::Check.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep check [options] [sources]"
opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
opts.on("--with-expectations[=PATH]", "Type check with expectations saved in PATH (or steep_expectations.yml)") do |path|
command.with_expectations_path = Pathname(path || "steep_expectations.yml")
end
opts.on("--save-expectations[=PATH]", "Save expectations with current type check result to PATH (or steep_expectations.yml)") do |path|
command.save_expectations_path = Pathname(path || "steep_expectations.yml")
end
opts.on("--severity-level=LEVEL", /^error|warning|information|hint$/, "Specify the minimum diagnostic severity to be recognized as an error (defaults: warning): error, warning, information, or hint") do |level|
command.severity_level = level.to_sym
end
handle_jobs_option command.jobs_option, opts
handle_logging_options opts
end.parse!(argv)
setup_jobs_for_ci(command.jobs_option)
command.command_line_patterns.push *argv
end.run
end
def process_checkfile
Drivers::Checkfile.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep checkfile [options] [files]"
opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
opts.on("--all-rbs", "Type check all RBS files") { command.all_rbs = true }
opts.on("--all-ruby", "Type check all Ruby files") { command.all_ruby = true }
opts.on("--stdin", "Read files to type check from stdin") do
while line = stdin.gets()
object = JSON.parse(line, symbolize_names: true)
Steep.logger.info { "Loading content of `#{object[:path]}` from stdin: #{object[:content].lines[0].chomp}" }
command.stdin_input[Pathname(object[:path])] = object[:content]
end
end
handle_jobs_option command.jobs_option, opts
handle_logging_options opts
end.parse!(argv)
setup_jobs_for_ci(command.jobs_option)
command.command_line_args.push *argv
end.run
end
def process_stats
Drivers::Stats.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep stats [options] [sources]"
opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
opts.on("--format=FORMAT", "Specify output format: csv, table") {|format| command.format = format }
handle_jobs_option command.jobs_option, opts
handle_logging_options opts
end.parse!(argv)
setup_jobs_for_ci(command.jobs_option)
command.command_line_patterns.push *argv
end.run
end
def process_validate
Drivers::Validate.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
handle_logging_options opts
end.parse!(argv)
end.run
end
def process_annotations
Drivers::Annotations.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep annotations [options] [sources]"
handle_logging_options opts
end.parse!(argv)
command.command_line_patterns.push *argv
end.run
end
def process_project
Drivers::PrintProject.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep project [options]"
opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
handle_logging_options opts
end.parse!(argv)
end.run
end
def process_watch
Drivers::Watch.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep watch [options] [dirs]"
opts.on("--severity-level=LEVEL", /^error|warning|information|hint$/, "Specify the minimum diagnostic severity to be recognized as an error (defaults: warning): error, warning, information, or hint") do |level|
# @type var level: String
command.severity_level = _ = level.to_sym
end
handle_jobs_option command.jobs_option, opts
handle_logging_options opts
end.parse!(argv)
setup_jobs_for_ci(command.jobs_option)
dirs = argv.map {|dir| Pathname(dir) }
command.dirs.push(*dirs)
end.run
end
def process_langserver
Drivers::Langserver.new(stdout: stdout, stderr: stderr, stdin: stdin).tap do |command|
OptionParser.new do |opts|
opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
handle_jobs_option command.jobs_option, opts
handle_logging_options opts
end.parse!(argv)
end.run
end
def process_vendor
Drivers::Vendor.new(stdout: stdout, stderr: stderr, stdin: stdin).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep vendor [options] [dir]"
handle_logging_options opts
opts.on("--[no-]clean") do |v|
command.clean_before = v
end
end.parse!(argv)
command.vendor_dir = Pathname(argv[0] || "vendor/sigs")
end.run
end
def process_binstub
path = Pathname("bin/steep")
root_path = Pathname.pwd
force = false
OptionParser.new do |opts|
opts.banner = <<BANNER
Usage: steep binstub [options]
Generate a binstub to execute Steep with setting up Bundler and rbenv/rvm.
Use the executable for LSP integration setup.
Options:
BANNER
handle_logging_options opts
opts.on("-o PATH", "--output=PATH", "The path of the executable file (defaults to `bin/steep`)") do |v|
path = Pathname(v)
end
opts.on("--root=PATH", "The repository root path (defaults to `.`)") do |v|
root_path = (Pathname.pwd + v).cleanpath
end
opts.on("--[no-]force", "Overwrite file (defaults to false)") do
force = true
end
end.parse!(argv)
binstub_path = (Pathname.pwd + path).cleanpath
bindir_path = binstub_path.parent
bindir_path.mkpath
gemfile_path =
if defined?(Bundler)
Bundler.default_gemfile.relative_path_from(bindir_path)
else
Pathname("../Gemfile")
end
if binstub_path.file?
if force
stdout.puts Rainbow("#{path} already exists. Overwriting...").yellow
else
stdout.puts Rainbow(''"⚠️ #{path} already exists. Bye! 👋").red
return 0
end
end
template = <<TEMPLATE
#!/usr/bin/env bash
BINSTUB_DIR=$(cd $(dirname $0); pwd)
GEMFILE=$(readlink -f ${BINSTUB_DIR}/#{gemfile_path})
ROOT_DIR=$(readlink -f ${BINSTUB_DIR}/#{root_path.relative_path_from(bindir_path)})
STEEP="bundle exec --gemfile=${GEMFILE} steep"
if type "rbenv" > /dev/null 2>&1; then
STEEP="rbenv exec ${STEEP}"
else
if type "rvm" > /dev/null 2>&1; then
if [ -e ${ROOT_DIR}/.ruby-version ]; then
STEEP="rvm ${ROOT_DIR} do ${STEEP}"
fi
fi
fi
exec $STEEP $@
TEMPLATE
binstub_path.write(template)
binstub_path.chmod(0755)
stdout.puts Rainbow("Successfully generated executable #{path} 🎉").blue
0
end
def process_version
stdout.puts Steep::VERSION
0
end
def process_worker
Drivers::Worker.new(stdout: stdout, stderr: stderr, stdin: stdin).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep worker [options] [dir]"
handle_logging_options opts
opts.on("--interaction") { command.worker_type = :interaction }
opts.on("--typecheck") { command.worker_type = :typecheck }
opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
opts.on("--name=NAME") {|name| command.worker_name = name }
opts.on("--delay-shutdown") { command.delay_shutdown = true }
opts.on("--max-index=COUNT") {|count| command.max_index = Integer(count) }
opts.on("--index=INDEX") {|index| command.index = Integer(index) }
end.parse!(argv)
command.commandline_args.push(*argv)
end.run
end
end
end