Files
brew/Library/Homebrew/ignorable.rb
T
Mike McQuaid 0c2baca69e ignorable: replace callcc with Fiber
- Ruby 4.0's `continuation` warns `callcc is obsolete; use Fiber
  instead` whenever it is required, which happens on every `brew`
  command that loads a formula from source.
- Run `Ignorable.hook_raise` blocks in a `Fiber`: `raise` now pauses
  at the raise site and asks an `on_ignorable` callback whether to
  resume (`:ignore`) or raise there as usual, replacing the rescue
  plus continuation jump and `Ignorable::ExceptionMixin#ignore`.
- `Debrew` menus and `Formulary` `ignore_errors` decisions now happen
  before the stack unwinds, so `ensure` blocks only run when an
  exception is actually raised.
- Only require `ignorable` when `Formulary` uses `ignore_errors` and
  drop the obsolete `brew verify-undefined` `Warnings` guard.

Fixes https://github.com/Homebrew/brew/issues/23384
2026-08-01 14:28:57 +01:00

64 lines
2.0 KiB
Ruby

# typed: strict
# frozen_string_literal: true
# Provides the ability to optionally ignore errors raised and continue execution.
module Ignorable
# Marks exceptions which can be ignored and resumed from where they were raised.
module ExceptionMixin; end
# Runs the block in a Fiber whose `raise` pauses at the raise site and passes
# the exception to `on_ignorable`. If it returns `:ignore`, execution resumes
# after the raise site, otherwise the exception is raised there as usual.
sig {
type_parameters(:U)
.params(
on_ignorable: T.proc.params(exception: Exception).returns(Symbol),
block: T.proc.returns(T.type_parameter(:U)),
)
.returns(T.type_parameter(:U))
}
def self.hook_raise(on_ignorable:, &block)
fiber = Fiber.new(&block)
Object.class_eval do
# `define_method` keeps Sorbet happy inside this `class_eval` block.
define_method(:raise) do |*args, **kwargs|
super(*args, **kwargs)
# All possible exceptions must be pausable, not just `StandardError`.
rescue Exception => e # rubocop:disable Lint/RescueException
if e.is_a?(ScriptError) || Fiber.current != fiber
super(e)
else
e.extend(ExceptionMixin)
super(e) if Fiber.yield(e) != :ignore
end
end
alias_method :fail, :raise
end
result = fiber.resume
while fiber.alive?
decision = begin
on_ignorable.call(result)
# Even `Interrupt` at the prompt must unwind the fiber, not abandon it.
rescue Exception => e # rubocop:disable Lint/RescueException
e
end
result = case decision
when :ignore then fiber.resume(:ignore)
# Raise inside the fiber so its `ensure` blocks and rescues still run.
when Exception then fiber.raise(decision)
else fiber.resume(:raise)
end
end
result
ensure
Object.class_eval do
remove_method(:raise)
remove_method(:fail)
end
end
end