Skip to content

Instantly share code, notes, and snippets.

@bf4
Created June 7, 2022 17:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bf4/16382acb22b645667b01d6efc35bd96c to your computer and use it in GitHub Desktop.
Save bf4/16382acb22b645667b01d6efc35bd96c to your computer and use it in GitHub Desktop.
warnings and Ruby 3.0

on CI and local

export RUBYOPT="-W:deprecated" # show warnings, deprecations only
require_path "lib", "warning"
# Ignore all warnings in Gem dependencies
Gem.path.each do |path|
Warning.ignore(//, path)
end
require "set"
RSpec.configure do |config|
warnings = Set.new
Warning.clear
Warning.ignore(:method_redefined)
Warning.ignore(%r{The called method `.+' is defined here}.freeze)
Warning.ignore(:missing_ivar)
Warning.ignore(:unused_var)
Warning.process do |warning|
warnings << warning
end
config.after(:suite) do
warnings = warnings
.to_a
.reject { |line| Gem.path.any? { |gem_path| line.start_with?(gem_path) } }
printable_warnings = warnings
.grep(/warning/)
.group_by { |line| line[/warning.*/] }
.map { |warning, lines| "#{warning}\n\t#{lines.map { |line| line[/\A\S+/] }.join("\n\t")}" }
# rubocop:disable Style/StderrPuts
if warnings.any? && printable_warnings.none?
$stderr.puts "---------------- WARNINGS -------------------"
$stderr.puts warnings
.join("\n")
$stderr.puts "---------------------------------------------"
end
if printable_warnings.any?
$stderr.puts "---------------- WARNINGS -------------------"
$stderr.puts printable_warnings
.join("\n\n")
$stderr.puts "---------------------------------------------"
end
# rubocop:enable Style/StderrPuts
end
end
# Adapted from https://github.com/jeremyevans/ruby-warning
# https://github.com/jeremyevans/ruby-warning/blob/6a97cadb7912b00411b5c81074ed67f7fc3d23fb/lib/warning.rb
# And see
# https://github.blog/2020-08-25-upgrading-github-to-ruby-2-7/
# https://github.com/Shopify/deprecation_toolkit/blob/2398f38acb62220fb79a6cd720f61d9cea26bc06/lib/deprecation_toolkit/warning.rb
# https://twitter.com/kddnewton/status/1258049996173332492
# Warning.singleton_class.prepend(
# Module.new do
# gems = %w[
# actionpack
# activemodel
# activerecord
# activesupport
# rspec
# sidekiq
# ]
# PATTERN =
# %r{gems/#{gems.join("|")}|The called method `.+' is defined here}.freeze
#
# def warn(warning)
# super unless warning.match?(PATTERN)
# end
# end
# )
# https://github.com/Betterment/uncruft
require "monitor"
module Warning
module Processor
# Map of symbols to regexps for warning messages to ignore.
IGNORE_MAP = {
ambiguous_slash: /: warning: ambiguous first argument; put parentheses or a space even after `\/' operator\n\z/,
arg_prefix: /: warning: `[&\*]' interpreted as argument prefix\n\z/,
bignum: /: warning: constant ::Bignum is deprecated\n\z/,
fixnum: /: warning: constant ::Fixnum is deprecated\n\z/,
method_redefined: /: warning: method redefined; discarding old .+\n\z|: warning: previous definition of .+ was here\n\z/,
missing_gvar: /: warning: global variable `\$.+' not initialized\n\z/,
missing_ivar: /: warning: instance variable @.+ not initialized\n\z/,
not_reached: /: warning: statement not reached\n\z/,
shadow: /: warning: shadowing outer local variable - \w+\n\z/,
unused_var: /: warning: assigned but unused variable - \w+\n\z/,
useless_operator: /: warning: possibly useless use of [><!=]+ in void context\n\z/,
keyword_separation: /: warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z/,
safe: /: warning: (?:rb_safe_level_2_warning|rb_safe_level|rb_set_safe_level_force|rb_set_safe_level|rb_secure|rb_insecure_operation|rb_check_safe_obj|\$SAFE) will (?:be removed|become a normal global variable) in Ruby 3\.0\n\z/,
taint: /: warning: (?:rb_error_untrusted|rb_check_trusted|Pathname#taint|Pathname#untaint|rb_env_path_tainted|Object#tainted\?|Object#taint|Object#untaint|Object#untrusted\?|Object#untrust|Object#trust|rb_obj_infect|rb_tainted_str_new|rb_tainted_str_new_cstr) is deprecated and will be removed in Ruby 3\.2\.\n\z/,
mismatched_indentations: /: warning: mismatched indentations at '.+' with '.+' at \d+\n\z/,
}
# Map of action symbols to procs that return the symbol
ACTION_PROC_MAP = {
default: proc { |_| :default },
backtrace: proc { |_| :backtrace },
raise: proc { |_| :raise },
}
private_constant :ACTION_PROC_MAP
# Clear all current ignored warnings, warning processors, and duplicate check cache.
# Also disables deduplicating warnings if that is currently enabled.
def clear
synchronize do
@ignore.clear
@process.clear
@dedup = false
end
end
# Deduplicate warnings, suppress warning messages if the same warning message
# has already occurred. Note that this can lead to unbounded memory use
# if unique warnings are generated.
def dedup
@dedup = {}
end
def freeze
@ignore.freeze
@process.freeze
super
end
# Ignore any warning messages matching the given regexp, if they
# start with the given path.
# The regexp can also be one of the following symbols (or an array including them), which will
# use an appropriate regexp for the given warning:
#
# :arg_prefix :: Ignore warnings when using * or & as an argument prefix
# :ambiguous_slash :: Ignore warnings for things like <tt>method /regexp/</tt>
# :bignum :: Ignore warnings when referencing the ::Bignum constant.
# :fixnum :: Ignore warnings when referencing the ::Fixnum constant.
# :keyword_separation :: Ignore warnings related to keyword argument separation.
# :method_redefined :: Ignore warnings when defining a method in a class/module where a
# method of the same name was already defined in that class/module.
# :missing_gvar :: Ignore warnings for accesses to global variables
# that have not yet been initialized
# :missing_ivar :: Ignore warnings for accesses to instance variables
# that have not yet been initialized
# :not_reached :: Ignore statement not reached warnings.
# :safe :: Ignore warnings related to $SAFE and related C-API functions.
# :shadow :: Ignore warnings related to shadowing outer local variables.
# :taint :: Ignore warnings related to taint and related methods and C-API functions.
# :unused_var :: Ignore warnings for unused variables.
# :useless_operator :: Ignore warnings when using operators such as == and > when the
# result is not used.
#
# Examples:
#
# # Ignore all uninitialized instance variable warnings
# Warning.ignore(/instance variable @\w+ not initialized/)
#
# # Ignore all uninitialized instance variable warnings in current file
# Warning.ignore(/instance variable @\w+ not initialized/, __FILE__)
#
# # Ignore all uninitialized instance variable warnings in current file
# Warning.ignore(:missing_ivar, __FILE__)
#
# # Ignore all uninitialized instance variable and method redefined warnings in current file
# Warning.ignore([:missing_ivar, :method_redefined], __FILE__)
def ignore(regexp, path = "")
unless regexp = convert_regexp(regexp)
raise TypeError, "first argument to Warning.ignore should be Regexp, Symbol, or Array of Symbols, got #{regexp.inspect}"
end
synchronize do
@ignore << [path, regexp]
end
nil
end
# Handle all warnings starting with the given path, instead of
# the default behavior of printing them to $stderr. Examples:
#
# # Write warning to LOGGER at level warning
# Warning.process do |warning|
# LOGGER.warning(warning)
# end
#
# # Write warnings in the current file to LOGGER at level error level
# Warning.process(__FILE__) do |warning|
# LOGGER.error(warning)
# end
#
# The block can return one of the following symbols:
#
# :default :: Take the default action (call super, printing to $stderr).
# :backtrace :: Take the default action (call super, printing to $stderr),
# and also print the backtrace.
# :raise :: Raise a RuntimeError with the warning as the message.
#
# If the block returns anything else, it is assumed the block completely handled
# the warning and takes no other action.
#
# Instead of passing a block, you can pass a hash of actions to take for specific
# warnings, using regexp as keys and a callable objects as values:
#
# Warning.process(__FILE__,
# /instance variable @\w+ not initialized/ => proc do |warning|
# LOGGER.warning(warning)
# end,
# /global variable `\$\w+' not initialized/ => proc do |warning|
# LOGGER.error(warning)
# end
# )
#
# Instead of passing a regexp as a key, you can pass a symbol that is recognized
# by Warning.ignore. Instead of passing a callable object as a value, you can
# pass a symbol, which will be treated as a callable object that returns that symbol:
#
# Warning.process(__FILE__, :missing_ivar=>:backtrace, :keyword_separation=>:raise)
def process(path = "", actions = nil, &block)
if block
if actions
raise ArgumentError, "cannot pass both actions and block to Warning.process"
end
elsif actions
block = {}
actions.each do |regexp, value|
unless regexp = convert_regexp(regexp)
raise TypeError, "action provided to Warning.process should be Regexp, Symbol, or Array of Symbols, got #{regexp.inspect}"
end
block[regexp] = ACTION_PROC_MAP[value] || value
end
else
raise ArgumentError, "must pass either actions or block to Warning.process"
end
synchronize do
@process << [path, block]
@process.sort_by!(&:first)
@process.reverse!
end
nil
end
# Handle ignored warnings and warning processors. If the warning is
# not ignored, is not a duplicate warning (if checking for duplicates)
# and there is no warning processor setup for the warning
# string, then use the default behavior of writing to $stderr.
def warn(str)
synchronize { @ignore.dup }.each do |path, regexp|
if str.start_with?(path) && str =~ regexp
return
end
end
if @dedup
if synchronize { @dedup[str] }
return
end
synchronize { @dedup[str] = true }
end
action = catch(:action) do
synchronize { @process.dup }.each do |path, block|
if str.start_with?(path)
if block.is_a?(Hash)
block.each do |regexp, blk|
if str&.match?(regexp)
throw :action, blk.call(str)
end
end
else
throw :action, block.call(str)
end
end
end
:default
end
case action
when :default
super
when :backtrace
super
$stderr.puts caller
when :raise
raise str
else
# nothing
end
nil
end
private
# Convert the given Regexp, Symbol, or Array of Symbols into a Regexp.
def convert_regexp(regexp)
case regexp
when Regexp
regexp
when Symbol
IGNORE_MAP.fetch(regexp)
when Array
Regexp.union(regexp.map { |re| IGNORE_MAP.fetch(re) })
else
# nothing
end
end
def synchronize(&block)
@monitor.synchronize(&block)
end
end
@ignore = []
@process = []
@dedup = false
@monitor = Monitor.new
extend Processor
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment