Allow ActiveSupport deprecation warnings to be configured as disallowed

This allows deprecation messages to be matched by substring, symbol (treated as
substring), or regular expression. If a warning is matched, the behaviors
configured for disallowed deprecations will be used. The default behavior for
disallowed deprecation warnings is `:raise`.

Also adds `ActiveSupport::Deprecation.allow` for thread-local, block level ignoring of deprecation warnings which would otherwise be disallowed by ActiveSupport::Deprecation.disallowed_warnings.
This commit is contained in:
Cliff Pruitt 2019-11-07 08:30:18 -05:00
parent 36e6a51662
commit 10754f79f3
10 changed files with 582 additions and 2 deletions

View File

@ -17,6 +17,7 @@ module ActiveSupport
require "active_support/deprecation/instance_delegator"
require "active_support/deprecation/behaviors"
require "active_support/deprecation/reporting"
require "active_support/deprecation/disallowed"
require "active_support/deprecation/constant_accessor"
require "active_support/deprecation/method_wrappers"
require "active_support/deprecation/proxy_wrappers"
@ -27,6 +28,7 @@ module ActiveSupport
include InstanceDelegator
include Behavior
include Reporting
include Disallowed
include MethodWrapper
# The version number in which the deprecated behavior will be removed, by default.
@ -43,6 +45,7 @@ module ActiveSupport
self.silenced = false
self.debug = false
@silenced_thread = Concurrent::ThreadLocalVar.new(false)
@explicitly_allowed_warnings = Concurrent::ThreadLocalVar.new(nil)
end
end
end

View File

@ -67,6 +67,11 @@ module ActiveSupport
@behavior ||= [DEFAULT_BEHAVIORS[:stderr]]
end
# Returns the current behavior for disallowed deprecations or if one isn't set, defaults to +:raise+.
def disallowed_behavior
@disallowed_behavior ||= [DEFAULT_BEHAVIORS[:raise]]
end
# Sets the behavior to the specified value. Can be a single value, array,
# or an object that responds to +call+.
#
@ -92,6 +97,14 @@ module ActiveSupport
@behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
end
# Sets the behavior for disallowed deprecations (those configured by
# ActiveSupport::Deprecation.disallowed_warnings=) to the specified
# value. As with +behavior=+, this can be a single value, array, or an
# object that responds to +call+.
def disallowed_behavior=(behavior)
@disallowed_behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
end
private
def arity_coerce(behavior)
unless behavior.respond_to?(:call)

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module ActiveSupport
class Deprecation
module Disallowed
# Sets the criteria used to identify deprecation messages which should be
# disallowed. Can be an array containing strings, symbols, or regular
# expressions. (Symbols are treated as strings). These are compared against
# the text of the generated deprecation warning.
#
# Additionally the scalar symbol +:all+ may be used to treat all
# deprecations as disallowed.
#
# Deprecations matching a substring or regular expression will be handled
# using the configured +ActiveSupport::Deprecation.disallowed_behavior+
# rather than +ActiveSupport::Deprecation.behavior+
attr_writer :disallowed_warnings
# Returns the configured criteria used to identify deprecation messages
# which should be treated as disallowed.
def disallowed_warnings
@disallowed_warnings ||= []
end
private
def deprecation_disallowed?(message)
disallowed = ActiveSupport::Deprecation.disallowed_warnings
return false if explicitly_allowed?(message)
return true if disallowed == :all
disallowed.any? do |rule|
case rule
when String, Symbol
message.include?(rule.to_s)
when Regexp
rule.match(message)
end
end
end
def explicitly_allowed?(message)
allowances = @explicitly_allowed_warnings.value
return false unless allowances
return true if allowances == :all
allowances = [allowances] unless allowances.kind_of?(Array)
allowances.any? do |rule|
case rule
when String, Symbol
message.include?(rule.to_s)
when Regexp
rule.match(message)
end
end
end
end
end
end

View File

@ -20,7 +20,11 @@ module ActiveSupport
callstack ||= caller_locations(2)
deprecation_message(callstack, message).tap do |m|
behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
if deprecation_disallowed?(message)
disallowed_behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
else
behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
end
end
end
@ -37,6 +41,44 @@ module ActiveSupport
@silenced_thread.bind(true, &block)
end
# Allow previously disallowed deprecation warnings within the block.
# <tt>allowed_warnings<tt> can be an array containing strings, symbols, or regular
# expressions. (Symbols are treated as strings). These are compared against
# the text of deprecation warning messages generated within the block.
# Matching warnings will be exempt from the rules set by
# +ActiveSupport::Deprecation.disallowed_warnings+
#
# The optional <tt>if:</tt> argument accepts a truthy/falsy value or an object that
# responds to <tt>.call</tt>. If truthy, then matching warnings will be allowed.
# If falsey then the method yields to the block without allowing the warning.
#
# ActiveSupport::Deprecation.disallowed_behavior = :raise
# ActiveSupport::Deprecation.disallowed_warnings = [
# "something broke"
# ]
#
# ActiveSupport::Deprecation.warn('something broke!')
# # => ActiveSupport::DeprecationException
#
# ActiveSupport::Deprecation.allow ['something broke'] do
# ActiveSupport::Deprecation.warn('something broke!')
# end
# # => nil
#
# ActiveSupport::Deprecation.allow ['something broke'], if: Rails.env.production? do
# ActiveSupport::Deprecation.warn('something broke!')
# end
# # => ActiveSupport::DeprecationException for dev/test, nil for production
def allow(allowed_warnings = :all, if: true, &block)
conditional = binding.local_variable_get(:if)
conditional = conditional.call if conditional.respond_to?(:call)
if conditional
@explicitly_allowed_warnings.bind(allowed_warnings, &block)
else
yield
end
end
def silenced
@silenced || @silenced_thread.value
end

View File

@ -28,6 +28,14 @@ module ActiveSupport
if deprecation = app.config.active_support.deprecation
ActiveSupport::Deprecation.behavior = deprecation
end
if disallowed_deprecation = app.config.active_support.disallowed_deprecation
ActiveSupport::Deprecation.disallowed_behavior = disallowed_deprecation
end
if disallowed_warnings = app.config.active_support.disallowed_deprecation_warnings
ActiveSupport::Deprecation.disallowed_warnings = disallowed_warnings
end
end
# Sets the default value for Time.zone

View File

@ -477,6 +477,433 @@ class DeprecationTest < ActiveSupport::TestCase
assert_deprecated { @dtc.g }
end
def test_config_disallows_no_deprecations_by_default
assert_equal ActiveSupport::Deprecation.disallowed_warnings, []
end
def test_allows_configuration_of_disallowed_warnings
resetting_disallowed_deprecation_config do
config_warnings = ["unsafe_method is going away"]
ActiveSupport::Deprecation.disallowed_warnings = config_warnings
assert_equal ActiveSupport::Deprecation.disallowed_warnings, config_warnings
end
end
def test_no_disallowed_behavior_with_no_disallowed_messages
resetting_disallowed_deprecation_config do
ActiveSupport::Deprecation.disallowed_behavior = :raise
@dtc.none
@dtc.partially
end
end
def test_disallowed_behavior_does_not_apply_to_allowed_messages
resetting_disallowed_deprecation_config do
ActiveSupport::Deprecation.disallowed_behavior = :raise
ActiveSupport::Deprecation.disallowed_warnings = ["foo=nil"]
@dtc.none
end
end
def test_disallowed_behavior_when_disallowed_message_configured_with_substring
resetting_disallowed_deprecation_config do
ActiveSupport::Deprecation.disallowed_behavior = :raise
ActiveSupport::Deprecation.disallowed_warnings = ["foo=nil"]
e = assert_raise ActiveSupport::DeprecationException do
@dtc.partially
end
message = "DEPRECATION WARNING: calling with foo=nil is out"
assert_match message, e.message
end
end
def test_disallowed_behavior_when_disallowed_message_configured_with_symbol_treated_as_substring
resetting_disallowed_deprecation_config do
ActiveSupport::Deprecation.disallowed_behavior = :raise
ActiveSupport::Deprecation.disallowed_warnings = [:foo]
e = assert_raise ActiveSupport::DeprecationException do
@dtc.partially
end
message = "DEPRECATION WARNING: calling with foo=nil is out"
assert_match message, e.message
end
end
def test_disallowed_behavior_when_disallowed_message_configured_with_regular_expression
resetting_disallowed_deprecation_config do
ActiveSupport::Deprecation.disallowed_behavior = :raise
ActiveSupport::Deprecation.disallowed_warnings = [/none|one*/]
e = assert_raise ActiveSupport::DeprecationException do
@dtc.none
end
message = "none is deprecated"
assert_match message, e.message
e = assert_raise ActiveSupport::DeprecationException do
@dtc.one
end
message = "one is deprecated"
assert_match message, e.message
end
end
def test_disallowed_behavior_when_disallowed_message_configured_with_scalar_symbol_all
resetting_disallowed_deprecation_config do
allowed_message = nil
disallowed_message = nil
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| allowed_message = msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| disallowed_message = msg }
]
ActiveSupport::Deprecation.disallowed_warnings = :all
@dtc.partially
assert_nil allowed_message
assert_match(/foo=nil/, disallowed_message)
allowed_message = nil
disallowed_message = nil
@dtc.none
assert_nil allowed_message
assert_match(/none is deprecated/, disallowed_message)
end
end
def test_different_behaviors_for_allowed_and_disallowed_messages
resetting_disallowed_deprecation_config do
@a, @b, @c, @d = nil, nil, nil, nil
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @a = msg },
lambda { |msg, callstack| @b = msg },
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @c = msg },
lambda { |msg, callstack| @d = msg },
]
ActiveSupport::Deprecation.disallowed_warnings = ["foo=nil"]
@dtc.partially
@dtc.none
assert_match(/none is deprecated/, @a)
assert_match(/none is deprecated/, @b)
assert_match(/foo=nil/, @c)
assert_match(/foo=nil/, @d)
end
end
def test_allow
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"b is deprecated",
"c is deprecated"
]
ActiveSupport::Deprecation.allow do
@dtc.a
@dtc.b
@dtc.c
end
assert_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/c is deprecated/, @warnings_allowed.join("\n"))
assert_empty @warnings_disallowed
end
end
def test_allow_only_matching_warnings
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"a is deprecated",
"b is deprecated",
"c is deprecated",
]
ActiveSupport::Deprecation.allow ["b is", "c is"] do
@dtc.none
@dtc.a
@dtc.b
@dtc.c
end
assert_match(/none is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/b is deprecated/, @warnings_allowed.join("\n"))
assert_match(/c is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/none is deprecated/, @warnings_disallowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
assert_no_match(/b is deprecated/, @warnings_disallowed.join("\n"))
assert_no_match(/c is deprecated/, @warnings_disallowed.join("\n"))
end
end
def test_allow_with_symbol
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"a is deprecated",
"b is deprecated",
"c is deprecated",
]
ActiveSupport::Deprecation.allow [:"b is", :"c is"] do
@dtc.none
@dtc.a
@dtc.b
@dtc.c
end
assert_match(/none is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/b is deprecated/, @warnings_allowed.join("\n"))
assert_match(/c is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/none is deprecated/, @warnings_disallowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
assert_no_match(/b is deprecated/, @warnings_disallowed.join("\n"))
assert_no_match(/c is deprecated/, @warnings_disallowed.join("\n"))
end
end
def test_allow_with_regexp
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"a is deprecated",
"b is deprecated",
"c is deprecated",
]
ActiveSupport::Deprecation.allow [/(b|c)\sis/] do
@dtc.none
@dtc.a
@dtc.b
@dtc.c
end
assert_match(/none is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/b is deprecated/, @warnings_allowed.join("\n"))
assert_match(/c is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/none is deprecated/, @warnings_disallowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
assert_no_match(/b is deprecated/, @warnings_disallowed.join("\n"))
assert_no_match(/c is deprecated/, @warnings_disallowed.join("\n"))
end
end
def test_allow_only_has_effect_inside_provided_block
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"a is deprecated"
]
ActiveSupport::Deprecation.allow "a is deprecated and will" do
@dtc.a
end
assert_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/a is deprecated/, @warnings_disallowed.join("\n"))
@warnings_allowed, @warnings_disallowed = [], []
@dtc.a
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
end
end
def test_allow_only_has_effect_on_the_thread_on_which_it_was_called
th1, th2 = nil, nil
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"a is deprecated"
]
th1 = Thread.new do
# barrier.wait
ActiveSupport::Deprecation.allow "a is deprecated and will" do
th2 = Thread.new do
@dtc.a
end
th2.join
end
end
th1.join
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
end
ensure
th1.kill
th2.kill
end
def test_is_a_noop_based_on_if_kwarg_truthy_or_falsey
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"a is deprecated"
]
ActiveSupport::Deprecation.allow "a is deprecated and will", if: true do
@dtc.a
end
assert_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/a is deprecated/, @warnings_disallowed.join("\n"))
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.allow "a is deprecated and will", if: Object.new do
@dtc.a
end
assert_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/a is deprecated/, @warnings_disallowed.join("\n"))
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.allow "a is deprecated and will", if: false do
@dtc.a
end
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.allow "a is deprecated and will", if: nil do
@dtc.a
end
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
end
end
def test_is_a_noop_based_on_if_kwarg_using_proc
resetting_disallowed_deprecation_config do
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_allowed << msg }
]
ActiveSupport::Deprecation.disallowed_behavior = [
lambda { |msg, callstack, horizon, gem| @warnings_disallowed << msg }
]
ActiveSupport::Deprecation.disallowed_warnings = [
"a is deprecated"
]
ActiveSupport::Deprecation.allow "a is deprecated and will", if: Proc.new { true } do
@dtc.a
end
assert_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_no_match(/a is deprecated/, @warnings_disallowed.join("\n"))
@warnings_allowed, @warnings_disallowed = [], []
ActiveSupport::Deprecation.allow "a is deprecated and will", if: Proc.new { false } do
@dtc.a
end
assert_no_match(/a is deprecated/, @warnings_allowed.join("\n"))
assert_match(/a is deprecated/, @warnings_disallowed.join("\n"))
end
end
private
def deprecator_with_messages
klass = Class.new(ActiveSupport::Deprecation)
@ -487,4 +914,13 @@ class DeprecationTest < ActiveSupport::TestCase
end
deprecator
end
def resetting_disallowed_deprecation_config
original_deprecations = ActiveSupport::Deprecation.disallowed_warnings
original_behaviors = ActiveSupport::Deprecation.disallowed_behavior
yield
ensure
ActiveSupport::Deprecation.disallowed_warnings = original_deprecations
ActiveSupport::Deprecation.disallowed_behavior = original_behaviors
end
end

View File

@ -797,6 +797,10 @@ There are a few configuration options available in Active Support:
* `ActiveSupport::Deprecation.behavior` alternative setter to `config.active_support.deprecation` which configures the behavior of deprecation warnings for Rails.
* `ActiveSupport::Deprecation.disallowed_behavior` alternative setter to `config.active_support.disallowed_deprecation` which configures the behavior of disallowed deprecation warnings for Rails.
* `ActiveSupport::Deprecation.disallowed_warnings` alternative setter to `config.active_support.disallowed_deprecation_warnings` which configures deprecation warnings that the Application considers disallowed. This allows, for example, specific deprecations to be treated as hard failures.
* `ActiveSupport::Deprecation.silence` takes a block in which all deprecation warnings are silenced.
* `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. The default is `false`.
@ -1425,7 +1429,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `i18n.callbacks`: In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request.
* `active_support.deprecation_behavior`: Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production, and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values.
* `active_support.deprecation_behavior`: Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production, and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values. This initializer also sets up behaviors for disallowed deprecations, defaulting to `:raise` for development and test and `:log` for production. Disallowed deprecation warnings default to an empty array.
* `active_support.initialize_time_zone`: Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC".

View File

@ -45,6 +45,12 @@ Rails.application.configure do
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations
config.active_support.disallowed_deprecation = :raise
# Tell ActiveSupport which deprecating messages to disallow
config.active_support.disallowed_deprecation_warnings = []
<%- unless options.skip_active_record? -%>
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load

View File

@ -83,6 +83,12 @@ Rails.application.configure do
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Log disallowed deprecations
config.active_support.disallowed_deprecation = :log
# Tell ActiveSupport which deprecating messages to disallow
config.active_support.disallowed_deprecation_warnings = []
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new

View File

@ -54,6 +54,12 @@ Rails.application.configure do
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raise exceptions for disallowed deprecations
config.active_support.disallowed_deprecation = :raise
# Tell ActiveSupport which deprecating messages to disallow
config.active_support.disallowed_deprecation_warnings = []
# Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
end