Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove OpenStruct from CLI::Args #18847

Merged
merged 11 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Library/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,6 @@ Style/NumericLiterals:
Style/OpenStructUse:
Exclude:
- "Taps/**/*"
# TODO: This is a pre-existing violation and should be corrected
# to define methods so that call sites can be type-checked.
- "Homebrew/cli/args.rb"
- "Homebrew/cli/args.rbi"

Style/OptionalBooleanParameter:
AllowedMethods:
Expand Down
2 changes: 2 additions & 0 deletions Library/Homebrew/cask/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def self.defaults

sig { params(args: Homebrew::CLI::Args).returns(T.attached_class) }
def self.from_args(args)
# FIXME: T.unsafe is a workaround for methods that are only defined when `cask_options`
# is invoked on the parser. (These could be captured by a DSL compiler instead.)
args = T.unsafe(args)
new(explicit: {
appdir: args.appdir,
Expand Down
82 changes: 36 additions & 46 deletions Library/Homebrew/cli/args.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
# typed: strict
# frozen_string_literal: true

require "ostruct"

module Homebrew
module CLI
class Args < OpenStruct
class Args
# 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]] }

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

sig { void }
def initialize
require "cli/named_args"

super

@cli_args = T.let(nil, T.nilable(T::Array[String]))
@processed_options = T.let([], OptionsType)
@options_only = T.let([], T::Array[String])
Expand All @@ -30,30 +27,41 @@ def initialize

# 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] = []
@named = T.let(NamedArgs.new(parent: self), T.nilable(NamedArgs))
@remaining = T.let([], T::Array[String])
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
def freeze_remaining_args!(remaining_args) = @remaining.replace(remaining_args).freeze
dduugg marked this conversation as resolved.
Show resolved Hide resolved

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?]
options[:override_spec] = :head if self[:HEAD?]
options[:flags] = flags_only unless flags_only.empty?
self[:named] = NamedArgs.new(
*named_args.freeze,
parent: self,
cask_options:,
without_api:,
**options,
@named = T.let(
NamedArgs.new(
*named_args.freeze,
cask_options:,
flags: flags_only,
force_bottle: @table[:force_bottle?] || false,
override_spec: @table[:HEAD?] ? :head : nil,
parent: self,
without_api:,
),
T.nilable(NamedArgs),
)
end

sig { params(name: Symbol, value: T.untyped).void }
def set_arg(name, value)
@table[name] = value
end

sig { override.params(_blk: T.nilable(T.proc.params(x: T.untyped).void)).returns(T.untyped) }
def tap(&_blk)
dduugg marked this conversation as resolved.
Show resolved Hide resolved
return super if block_given? # Object#tap

@table[:tap]
end

sig { params(processed_options: OptionsType).void }
def freeze_processed_options!(processed_options)
# Reset cache values reliant on processed_options
Expand All @@ -69,15 +77,15 @@ def freeze_processed_options!(processed_options)
sig { returns(NamedArgs) }
def named
require "formula"
self[:named]
T.must(@named)
end

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

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

sig { returns(T::Array[String]) }
def include_test_formulae
if include_test?
if @table[:include_test?]
named.to_formulae.map(&:full_name)
else
[]
Expand All @@ -109,9 +117,9 @@ def context

sig { returns(T.nilable(Symbol)) }
def only_formula_or_cask
if formula? && !cask?
if @table[:formula?] && !@table[:cask?]
:formula
elsif cask? && !formula?
elsif @table[:cask?] && !@table[:formula?]
:cask
end
end
Expand All @@ -120,7 +128,7 @@ def only_formula_or_cask
def os_arch_combinations
skip_invalid_combinations = false

oses = case (os_sym = os&.to_sym)
oses = case (os_sym = @table[:os]&.to_sym)
when nil
[SimulateSystem.current_os]
when :all
Expand All @@ -131,7 +139,7 @@ def os_arch_combinations
[os_sym]
end

arches = case (arch_sym = arch&.to_sym)
arches = case (arch_sym = @table[:arch]&.to_sym)
when nil
[SimulateSystem.current_arch]
when :all
Expand Down Expand Up @@ -174,24 +182,6 @@ def cli_args
end
end.freeze
end

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

# Once we are frozen, verify any arg method calls are already defined in the table.
# The default OpenStruct behaviour is to return nil for anything unknown.
if frozen? && args.empty? && !@table.key?(method_name)
raise NoMethodError, "CLI arg for `#{method_name}` is not declared for this command"
end

return_value
end
end
end
end
24 changes: 0 additions & 24 deletions Library/Homebrew/cli/args.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,6 @@ class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def quiet?; end

sig { returns(T::Array[String]) }
def remaining; end

sig { returns(T::Boolean) }
def verbose?; end

# FIXME: The methods below are not defined by Args, but are valid because Args inherits from OpenStruct
# We should instead be using type guards to check if the method is defined on the object before calling it

sig { returns(T.nilable(String)) }
def arch; end

sig { returns(T::Boolean) }
def build_from_source?; end

sig { returns(T::Boolean) }
def cask?; end

sig { returns(T::Boolean) }
def formula?; end

sig { returns(T::Boolean) }
def include_test?; end

sig { returns(T.nilable(String)) }
def os; end
end
26 changes: 20 additions & 6 deletions Library/Homebrew/cli/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def comma_array(name, description: nil, hidden: false)
description = option_description(description, name, hidden:)
process_option(name, description, type: :comma_array, hidden:)
@parser.on(name, OptionParser::REQUIRED_ARGUMENT, Array, *wrap_option_desc(description)) do |list|
@args[option_to_name(name)] = list
set_args_method(option_to_name(name).to_sym, list)
end
end

Expand All @@ -277,7 +277,7 @@ def flag(*names, description: nil, replacement: nil, depends_on: nil, hidden: fa
# This odisabled should stick around indefinitely.
odisabled "the `#{names.first}` flag", replacement unless replacement.nil?
names.each do |name|
@args[option_to_name(name)] = option_value
set_args_method(option_to_name(name).to_sym, option_value)
end
end

Expand All @@ -286,6 +286,17 @@ def flag(*names, description: nil, replacement: nil, depends_on: nil, hidden: fa
end
end

sig { params(name: Symbol, value: T.untyped).void }
def set_args_method(name, value)
@args.set_arg(name, value)
return if @args.respond_to?(name)

@args.define_singleton_method(name) do
# We cannot reference the ivar directly due to https://github.com/sorbet/sorbet/issues/8106
instance_variable_get(:@table).fetch(name)
end
end

sig { params(options: String).returns(T::Array[T::Array[String]]) }
def conflicts(*options)
@conflicts << options.map { |option| option_to_name(option) }
Expand Down Expand Up @@ -558,24 +569,27 @@ def generate_banner
def set_switch(*names, value:, from:)
names.each do |name|
@switch_sources[option_to_name(name)] = from
@args["#{option_to_name(name)}?"] = value
set_args_method(:"#{option_to_name(name)}?", value)
end
end

sig { params(args: String).void }
def disable_switch(*args)
args.each do |name|
@args["#{option_to_name(name)}?"] = if name.start_with?("--[no-]")
result = if name.start_with?("--[no-]")
nil
else
false
end
set_args_method(:"#{option_to_name(name)}?", result)
end
end

sig { params(name: String).returns(T::Boolean) }
def option_passed?(name)
!!(@args[name.to_sym] || @args[:"#{name}?"])
[name.to_sym, :"#{name}?"].any? do |method|
@args.public_send(method) if @args.respond_to?(method)
dduugg marked this conversation as resolved.
Show resolved Hide resolved
end
end

sig { params(desc: String).returns(T::Array[String]) }
Expand Down Expand Up @@ -676,7 +690,7 @@ def process_option(*args, type:, hidden: false)
disable_switch(*args)
else
args.each do |name|
@args[option_to_name(name)] = nil
set_args_method(option_to_name(name).to_sym, nil)
end
end

Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/cmd/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def print_regex_help

sig { returns(T::Boolean) }
def search_package_manager
package_manager = PACKAGE_MANAGERS.find { |name,| args[:"#{name}?"] }
package_manager = PACKAGE_MANAGERS.find { |name,| args.public_send(:"#{name}?") }
return false if package_manager.nil?

_, url = package_manager
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/dev-cmd/audit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def run

audit_formulae, audit_casks = Homebrew.with_no_api_env do # audit requires full Ruby source
if args.tap
Tap.fetch(T.must(args.tap)).then do |tap|
Tap.fetch(args.tap).then do |tap|
[
tap.formula_files.map { |path| Formulary.factory(path) },
tap.cask_files.map { |path| Cask::CaskLoader.load(path) },
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/dev-cmd/bump.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def run
Formulary.factory(qualified_name)
end
elsif args.tap
tap = Tap.fetch(T.must(args.tap))
tap = Tap.fetch(args.tap)
raise UsageError, "`--tap` requires `--auto` for official taps." if tap.official?

formulae = args.cask? ? [] : tap.formula_files.map { |path| Formulary.factory(path) }
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/dev-cmd/livecheck.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def run

formulae_and_casks_to_check = Homebrew.with_no_api_env do
if args.tap
tap = Tap.fetch(T.must(args.tap))
tap = Tap.fetch(args.tap)
formulae = args.cask? ? [] : tap.formula_files.map { |path| Formulary.factory(path) }
casks = args.formula? ? [] : tap.cask_files.map { |path| Cask::CaskLoader.load(path) }
formulae + casks
Expand Down
4 changes: 2 additions & 2 deletions Library/Homebrew/extend/os/linux/cli/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ module Parser

sig { void }
def set_default_options
args["formula?"] = true if args.respond_to?(:formula?)
args.set_arg(:formula?, true)
end

sig { void }
def validate_options
return unless args.respond_to?(:cask?)
return unless args.cask?
return unless T.unsafe(self).args.cask?
dduugg marked this conversation as resolved.
Show resolved Hide resolved

# NOTE: We don't raise an error here because we don't want
# to print the help page or a stack trace.
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/tap_cmd.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/audit.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/bump.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/create.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading