Skip to content

Commit cd181b7

Browse files
committed
Normalize paths in deprecation messages
1 parent b6720a9 commit cd181b7

File tree

5 files changed

+198
-5
lines changed

5 files changed

+198
-5
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## main (unreleased)
44

5+
* [#60](https://github.com/Shopify/deprecation_toolkit/pull/60): Normalize paths in deprecation messages. (@sambostock)
6+
57
## 2.0.0 (2022-03-16)
68

79
* [#58](https://github.com/Shopify/deprecation_toolkit/pull/58): Drop support for Ruby < 2.6 & Active Support < 5.2. (@sambostock)

Diff for: README.md

+32
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,38 @@ This setting accepts an array of regular expressions. To match on all warnings,
120120
DeprecationToolkit::Configuration.warnings_treated_as_deprecation = [//]
121121
```
122122

123+
### 🔨 `#DeprecationToolkit::Configuration#message_normalizers`
124+
125+
Deprecation Toolkit allows the normalization of deprecation warnings by registering message normalizers.
126+
127+
Out-of-the-box, various path normalizers are included, to ensure that any paths included in deprecation warnings are consistent from machine to machine (see [`lib/deprecation_toolkit/configuration.rb`](lib/deprecation_toolkit/configuration.rb) for details).
128+
129+
### Customizing normalization
130+
131+
Additional normalizers can be added by appending then to the array:
132+
133+
```ruby
134+
DeprecationToolkit::Configuration.message_normalizers << ->(msg) { msg.downcase }
135+
```
136+
137+
Normalizers are expected to respond to `.call`, accepting a `String` and returning a normalized `String`, and are applied in order of registration.
138+
139+
If you wish to normalize a custom path, you can create your own `DeprecationToolkit::PathPrefixNormalizer`:
140+
141+
```ruby
142+
DeprecationToolkit::Configuration.message_normalizers <<
143+
DeprecationToolkit::PathPrefixNormalizer.new(
144+
'/path/to/something',
145+
replacement: 'optional replacement string',
146+
)
147+
```
148+
149+
You may optionally disable any or all normalizers by mutating or replacing the array.
150+
151+
```ruby
152+
DeprecationToolkit::Configuration.message_normalizers = [] # remove all normalizers
153+
```
154+
123155
## RSpec
124156

125157
By default Deprecation Toolkit uses Minitest as its test runner. To use Deprecation Toolkit with RSpec you'll have to configure it.

Diff for: lib/deprecation_toolkit/configuration.rb

+30
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,35 @@ class Configuration
1212
config_accessor(:deprecation_path) { "test/deprecations" }
1313
config_accessor(:test_runner) { :minitest }
1414
config_accessor(:warnings_treated_as_deprecation) { [] }
15+
16+
config_accessor(:message_normalizers) do
17+
normalizers = []
18+
19+
normalizers << PathPrefixNormalizer.new(Rails.root) if defined?(Rails)
20+
normalizers << PathPrefixNormalizer.new(Bundler.root) if defined?(Bundler)
21+
22+
if defined?(Gem)
23+
Gem.loaded_specs.each_value do |spec|
24+
normalizers << PathPrefixNormalizer.new(spec.bin_dir, replacement: "<GEM_BIN_DIR:#{spec.name}>")
25+
normalizers << PathPrefixNormalizer.new(spec.extension_dir, replacement: "<GEM_EXTENSION_DIR:#{spec.name}>")
26+
normalizers << PathPrefixNormalizer.new(spec.gem_dir, replacement: "<GEM_DIR:#{spec.name}>")
27+
end
28+
normalizers << PathPrefixNormalizer.new(*Gem.path, replacement: "<GEM_PATH>")
29+
end
30+
31+
begin
32+
require "rbconfig"
33+
normalizers << PathPrefixNormalizer.new(
34+
*RbConfig::CONFIG.values_at('prefix', 'sitelibdir', 'rubylibdir'),
35+
replacement: "<RUBY_INTERNALS>",
36+
)
37+
rescue LoadError
38+
# skip normalizing ruby internal paths
39+
end
40+
41+
normalizers << PathPrefixNormalizer.new(Dir.pwd)
42+
43+
normalizers
44+
end
1545
end
1646
end

Diff for: lib/deprecation_toolkit/deprecation_subscriber.rb

+11-5
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,24 @@ def self.already_attached?
99
end
1010

1111
def deprecation(event)
12-
message = event.payload[:message]
12+
message = normalize_message(event.payload[:message])
1313

14-
Collector.collect(message) unless deprecation_allowed?(event.payload)
14+
Collector.collect(message) unless deprecation_allowed?(message, event.payload[:callstack])
1515
end
1616

1717
private
1818

19-
def deprecation_allowed?(payload)
19+
def deprecation_allowed?(message, callstack)
2020
allowed_deprecations, procs = Configuration.allowed_deprecations.partition { |el| el.is_a?(Regexp) }
2121

22-
allowed_deprecations.any? { |regex| regex =~ payload[:message] } ||
23-
procs.any? { |proc| proc.call(payload[:message], payload[:callstack]) }
22+
allowed_deprecations.any? { |regex| regex =~ message } ||
23+
procs.any? { |proc| proc.call(message, callstack) }
24+
end
25+
26+
def normalize_message(message)
27+
Configuration
28+
.message_normalizers
29+
.reduce(message) { |message, normalizer| normalizer.call(message) }
2430
end
2531
end
2632
end

Diff for: test/deprecation_toolkit/warning_test.rb

+123
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,128 @@ class WarningTest < ActiveSupport::TestCase
8282
assert_match(/Using the last argument as keyword parameters/, error.message)
8383
assert_match(/The called method/, error.message)
8484
end
85+
86+
test "'Ruby 2.7 last argument as keyword parameters' real deprecation warning is handled with normalized paths" do
87+
Configuration.warnings_treated_as_deprecation = [/Using the last argument as keyword parameters/]
88+
89+
error = assert_raises(Behaviors::DeprecationIntroduced) do
90+
warn("#{Dir.pwd}/path/to/caller.rb:1: warning: Using the last argument as keyword parameters is deprecated; " \
91+
"maybe ** should be added to the call")
92+
warn("#{Dir.pwd}/path/to/callee.rb:1: warning: The called method `method_name' is defined here")
93+
94+
trigger_deprecation_toolkit_behavior
95+
end
96+
97+
assert_match(%r{^DEPRECATION WARNING: path/to/caller\.rb:1: warning: Using the last},
98+
error.message)
99+
assert_match(%r{^path/to/callee\.rb:1: warning: The called method}, error.message)
100+
end
101+
102+
test "`assert_nil` real deprecation warning is handled with normalized paths" do
103+
Configuration.warnings_treated_as_deprecation = [/Use assert_nil if expecting nil/]
104+
105+
error = assert_raises(Behaviors::DeprecationIntroduced) do
106+
warn("Use assert_nil if expecting nil from #{Dir.pwd}/path/to/file.rb:1. This will fail in Minitest 6.")
107+
108+
trigger_deprecation_toolkit_behavior
109+
end
110+
111+
assert_match(
112+
%r{^DEPRECATION WARNING: Use assert_nil if expecting nil from path/to/file\.rb:1}, error.message
113+
)
114+
end
115+
116+
test "the path to warn itself is handled too" do
117+
Configuration.warnings_treated_as_deprecation = [/boom/]
118+
119+
error = assert_raises(Behaviors::DeprecationIntroduced) do
120+
warn("boom")
121+
122+
trigger_deprecation_toolkit_behavior
123+
end
124+
125+
assert_includes(error.message, <<~MESSAGE.chomp)
126+
DEPRECATION WARNING: boom
127+
(called from call at <RUBY_INTERNALS>/rubygems/core_ext/kernel_warn.rb:22)
128+
MESSAGE
129+
end
130+
131+
test "Rails.root is normalized in deprecation messages" do
132+
rails_stub = Object.new
133+
rails_stub.define_singleton_method(:inspect) { "Rails (stub)" }
134+
rails_stub.define_singleton_method(:root) { "/path/to/rails/root" }
135+
136+
original_rails = defined?(::Rails) && ::Rails
137+
Object.const_set(:Rails, rails_stub)
138+
139+
assert_normalizes(
140+
from: "#{Rails.root}/app/models/whatever.rb",
141+
to: "app/models/whatever.rb",
142+
)
143+
ensure
144+
if original_rails.nil?
145+
Object.send(:remove_const, :Rails)
146+
else
147+
Object.const_set(:Rails, original_rails)
148+
end
149+
end
150+
151+
test "Bundler.root is normalized in deprecation messages" do
152+
assert_normalizes(
153+
from: "#{Bundler.root}/lib/whatever.rb",
154+
to: "lib/whatever.rb",
155+
)
156+
end
157+
158+
test "Gem spec gem_dirs are normalized in deprecation messages" do
159+
spec = Gem.loaded_specs.each_value.first
160+
assert_normalizes(
161+
from: "#{spec.gem_dir}/lib/whatever.rb",
162+
to: "<GEM_DIR:#{spec.name}>/lib/whatever.rb",
163+
)
164+
end
165+
166+
test "Gem spec extension_dirs are normalized in deprecation messages" do
167+
spec = Gem.loaded_specs.each_value.first
168+
assert_normalizes(
169+
from: "#{spec.extension_dir}/lib/whatever.rb",
170+
to: "<GEM_EXTENSION_DIR:#{spec.name}>/lib/whatever.rb",
171+
)
172+
end
173+
174+
test "Gem spec bin_dirs are normalized in deprecation messages" do
175+
spec = Gem.loaded_specs.each_value.first
176+
assert_normalizes(
177+
from: "#{spec.bin_dir}/lib/whatever.rb",
178+
to: "<GEM_BIN_DIR:#{spec.name}>/lib/whatever.rb",
179+
)
180+
end
181+
182+
test "Gem paths are normalized in deprecation messages" do
183+
paths = Gem.path
184+
assert_normalizes(
185+
from: paths.map.with_index { |path, index| "#{path}/file-#{index}" }.join("\n"),
186+
to: Array.new(paths.length) { |index| "<GEM_PATH>/file-#{index}" }.join("\n"),
187+
)
188+
end
189+
190+
test "RbConfig paths are normalized in deprecation messages" do
191+
paths = RbConfig::CONFIG.values_at('prefix', 'sitelibdir', 'rubylibdir').compact
192+
assert_normalizes(
193+
from: paths.map.with_index { |path, index| "#{path}/file-#{index}" }.join("\n"),
194+
to: Array.new(paths.length) { |index| "<RUBY_INTERNALS>/file-#{index}" }.join("\n"),
195+
)
196+
end
197+
198+
private
199+
200+
def assert_normalizes(from:, to:)
201+
Configuration.warnings_treated_as_deprecation = [/test deprecation/]
202+
error = assert_raises(Behaviors::DeprecationIntroduced) do
203+
warn("test deprecation: #{from}.")
204+
trigger_deprecation_toolkit_behavior
205+
end
206+
assert_includes(error.message, to)
207+
end
85208
end
86209
end

0 commit comments

Comments
 (0)