Skip to content

Commit

Permalink
Merge pull request #18083 from Homebrew/strict-args
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeMcQuaid committed Aug 20, 2024
2 parents 954f240 + 7acddd7 commit a4d5dc2
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 17 deletions.
45 changes: 34 additions & 11 deletions Library/Homebrew/cli/args.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
# typed: true # rubocop:disable Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true

require "ostruct"

module Homebrew
module CLI
class Args < OpenStruct
# FIXME: Enable cop again when https://github.com/sorbet/sorbet/issues/3532 is fixed.
# rubocop:disable Style/MutableConstant
# Represents a processed option. The array elements are:
# 0: short option name (e.g. "-d")
# 1: long option name (e.g. "--debug")
# 2: option description (e.g. "Print debugging information")
# 3: whether the option is hidden
OptionsType = T.type_alias { T::Array[[String, T.nilable(String), String, T::Boolean]] }
# rubocop:enable Style/MutableConstant

sig { returns(T::Array[String]) }
attr_reader :options_only, :flags_only

# undefine tap to allow --tap argument
Expand All @@ -17,21 +28,25 @@ def initialize

super

@processed_options = []
@options_only = []
@flags_only = []
@cask_options = false
@cli_args = T.let(nil, T.nilable(T::Array[String]))
@processed_options = T.let([], OptionsType)
@options_only = T.let([], T::Array[String])
@flags_only = T.let([], T::Array[String])
@cask_options = T.let(false, T::Boolean)
@table = T.let({}, T::Hash[Symbol, T.untyped])

# Can set these because they will be overwritten by freeze_named_args!
# (whereas other values below will only be overwritten if passed).
self[:named] = NamedArgs.new(parent: self)
self[:remaining] = []
end

sig { params(remaining_args: T::Array[T.any(T::Array[String], String)]).void }
def freeze_remaining_args!(remaining_args)
self[:remaining] = remaining_args.freeze
end

sig { params(named_args: T::Array[String], cask_options: T::Boolean, without_api: T::Boolean).void }
def freeze_named_args!(named_args, cask_options:, without_api:)
options = {}
options[:force_bottle] = true if self[:force_bottle?]
Expand All @@ -46,15 +61,16 @@ def freeze_named_args!(named_args, cask_options:, without_api:)
)
end

sig { params(processed_options: OptionsType).void }
def freeze_processed_options!(processed_options)
# Reset cache values reliant on processed_options
@cli_args = nil

@processed_options += processed_options
@processed_options.freeze

@options_only = cli_args.select { |a| a.start_with?("-") }.freeze
@flags_only = cli_args.select { |a| a.start_with?("--") }.freeze
@options_only = cli_args.select { _1.start_with?("-") }.freeze
@flags_only = cli_args.select { _1.start_with?("--") }.freeze
end

sig { returns(NamedArgs) }
Expand All @@ -63,10 +79,10 @@ def named
self[:named]
end

def no_named?
named.blank?
end
sig { returns(T::Boolean) }
def no_named? = named.blank?

sig { returns(T::Array[String]) }
def build_from_source_formulae
if build_from_source? || self[:HEAD?] || self[:build_bottle?]
named.to_formulae.map(&:full_name)
Expand All @@ -75,6 +91,7 @@ def build_from_source_formulae
end
end

sig { returns(T::Array[String]) }
def include_test_formulae
if include_test?
named.to_formulae.map(&:full_name)
Expand All @@ -83,6 +100,7 @@ def include_test_formulae
end
end

sig { params(name: String).returns(T.nilable(String)) }
def value(name)
arg_prefix = "--#{name}="
flag_with_value = flags_only.find { |arg| arg.start_with?(arg_prefix) }
Expand All @@ -96,6 +114,7 @@ def context
Context::ContextStruct.new(debug: debug?, quiet: quiet?, verbose: verbose?)
end

sig { returns(T.nilable(Symbol)) }
def only_formula_or_cask
if formula? && !cask?
:formula
Expand Down Expand Up @@ -141,11 +160,13 @@ def os_arch_combinations

private

sig { params(option: String).returns(String) }
def option_to_name(option)
option.sub(/\A--?/, "")
.tr("-", "_")
end

sig { returns(T::Array[String]) }
def cli_args
return @cli_args if @cli_args

Expand All @@ -165,10 +186,12 @@ def cli_args
@cli_args.freeze
end

def respond_to_missing?(method_name, *)
sig { params(method_name: Symbol, _include_private: T::Boolean).returns(T::Boolean) }
def respond_to_missing?(method_name, _include_private = false)
@table.key?(method_name)
end

sig { params(method_name: Symbol, args: T.untyped).returns(T.untyped) }
def method_missing(method_name, *args)
return_value = super

Expand Down
7 changes: 3 additions & 4 deletions Library/Homebrew/cli/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class Parser
# FIXME: Enable cop again when https://github.com/sorbet/sorbet/issues/3532 is fixed.
# rubocop:disable Style/MutableConstant
ArgType = T.type_alias { T.any(NilClass, Symbol, T::Array[String], T::Array[Symbol]) }
OptionsType = T.type_alias { T::Array[[String, T.nilable(String), T.nilable(String), String, T::Boolean]] }
# rubocop:enable Style/MutableConstant
HIDDEN_DESC_PLACEHOLDER = "@@HIDDEN@@"
SYMBOL_TO_USAGE_MAPPING = T.let({
Expand All @@ -25,7 +24,7 @@ class Parser
}.freeze, T::Hash[Symbol, String])
private_constant :ArgType, :HIDDEN_DESC_PLACEHOLDER, :SYMBOL_TO_USAGE_MAPPING

sig { returns(OptionsType) }
sig { returns(Args::OptionsType) }
attr_reader :processed_options

sig { returns(T::Boolean) }
Expand Down Expand Up @@ -177,7 +176,7 @@ def initialize(cmd = nil, &block)
@constraints = T.let([], T::Array[[String, String]])
@conflicts = T.let([], T::Array[T::Array[String]])
@switch_sources = T.let({}, T::Hash[String, Symbol])
@processed_options = T.let([], OptionsType)
@processed_options = T.let([], Args::OptionsType)
@non_global_processed_options = T.let([], T::Array[[String, ArgType]])
@named_args_type = T.let(nil, T.nilable(ArgType))
@max_named_args = T.let(nil, T.nilable(Integer))
Expand Down Expand Up @@ -671,7 +670,7 @@ def check_named_args(args)
def process_option(*args, type:, hidden: false)
option, = @parser.make_switch(args)
@processed_options.reject! { |existing| existing.second == option.long.first } if option.long.first.present?
@processed_options << [option.short.first, option.long.first, option.arg, option.desc.first, hidden]
@processed_options << [option.short.first, option.long.first, option.desc.first, hidden]

args.pop # last argument is the description
if type == :switch
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def self.command_options(command)
return if path.blank?

if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path))
cmd_parser.processed_options.filter_map do |short, long, _, desc, hidden|
cmd_parser.processed_options.filter_map do |short, long, desc, hidden|
next if hidden

[long || short, desc]
Expand Down
3 changes: 2 additions & 1 deletion Library/Homebrew/manpages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ def self.generate_cmd_manpages(cmd_paths)
man_page_lines.compact.join("\n")
end

sig { params(cmd_parser: CLI::Parser).returns(T::Array[String]) }
def self.cmd_parser_manpage_lines(cmd_parser)
lines = [format_usage_banner(cmd_parser.usage_banner_text)]
lines += cmd_parser.processed_options.filter_map do |short, long, _, desc, hidden|
lines += cmd_parser.processed_options.filter_map do |short, long, desc, hidden|
next if hidden

if long.present?
Expand Down

0 comments on commit a4d5dc2

Please sign in to comment.