Skip to content

Commit 05f0b16

Browse files
authored
Merge pull request rails#55230 from rails/fxn/first_clean_location
Implement ActiveSupport::BacktraceCleaner#first_clean_location
2 parents c5bb138 + a0ea187 commit 05f0b16

File tree

3 files changed

+94
-2
lines changed

3 files changed

+94
-2
lines changed

activesupport/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
* The new method `ActiveSupport::BacktraceCleaner#first_clean_location`
2+
returns the first clean location of the caller's call stack, or `nil`.
3+
Locations are `Thread::Backtrace::Location` objects. Useful when you want to
4+
report the application-level location where something happened as an object.
5+
6+
*Xavier Noria*
7+
18
* FileUpdateChecker and EventedFileUpdateChecker ignore changes in Gem.path now.
29

310
*Ermolaev Andrey*, *zzak*
411

512
* The new method `ActiveSupport::BacktraceCleaner#first_clean_frame` returns
613
the first clean frame of the caller's backtrace, or `nil`. Useful when you
7-
want to report the application-level location where something happened.
14+
want to report the application-level frame where something happened as a
15+
string.
816

917
*Xavier Noria*
1018

activesupport/lib/active_support/backtrace_cleaner.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def clean_frame(frame, kind = :silent)
7777
# Thread.each_caller_location does not accept a start in Ruby < 3.4.
7878
if Thread.method(:each_caller_location).arity == 0
7979
# Returns the first clean frame of the caller's backtrace, or +nil+.
80+
#
81+
# Frames are strings.
8082
def first_clean_frame(kind = :silent)
8183
caller_location_skipped = false
8284

@@ -92,6 +94,8 @@ def first_clean_frame(kind = :silent)
9294
end
9395
else
9496
# Returns the first clean frame of the caller's backtrace, or +nil+.
97+
#
98+
# Frames are strings.
9599
def first_clean_frame(kind = :silent)
96100
Thread.each_caller_location(2) do |location|
97101
frame = clean_frame(location, kind)
@@ -100,6 +104,34 @@ def first_clean_frame(kind = :silent)
100104
end
101105
end
102106

107+
# Thread.each_caller_location does not accept a start in Ruby < 3.4.
108+
if Thread.method(:each_caller_location).arity == 0
109+
# Returns the first clean location of the caller's call stack, or +nil+.
110+
#
111+
# Locations are Thread::Backtrace::Location objects.
112+
def first_clean_location(kind = :silent)
113+
caller_location_skipped = false
114+
115+
Thread.each_caller_location do |location|
116+
unless caller_location_skipped
117+
caller_location_skipped = true
118+
next
119+
end
120+
121+
return location if clean_frame(location, kind)
122+
end
123+
end
124+
else
125+
# Returns the first clean location of the caller's call stack, or +nil+.
126+
#
127+
# Locations are Thread::Backtrace::Location objects.
128+
def first_clean_location(kind = :silent)
129+
Thread.each_caller_location(2) do |location|
130+
return location if clean_frame(location, kind)
131+
end
132+
end
133+
end
134+
103135
# Adds a filter from the block provided. Each line in the backtrace will be
104136
# mapped against this filter.
105137
#

activesupport/test/backtrace_cleaner_test.rb

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def setup
137137
end
138138
end
139139

140-
class BacktraceCleanerFirstCleanFrame < ActiveSupport::TestCase
140+
class BacktraceCleanerFirstCleanFrameTest < ActiveSupport::TestCase
141141
def setup
142142
@bc = ActiveSupport::BacktraceCleaner.new
143143
end
@@ -180,3 +180,55 @@ def invoke_first_clean_frame(kind = :silent)
180180
assert_nil invoke_first_clean_frame_defaults
181181
end
182182
end
183+
184+
class BacktraceCleanerFirstCleanLocationTest < ActiveSupport::TestCase
185+
def setup
186+
@bc = ActiveSupport::BacktraceCleaner.new
187+
end
188+
189+
def invoke_first_clean_location_defaults
190+
-> do
191+
@bc.first_clean_location.tap { @line = __LINE__ + 1 }
192+
end.call
193+
end
194+
195+
def invoke_first_clean_location(kind = :silent)
196+
-> do
197+
@bc.first_clean_location(kind).tap { @line = __LINE__ + 1 }
198+
end.call
199+
end
200+
201+
test "returns the first clean location (defaults)" do
202+
location = invoke_first_clean_location_defaults
203+
204+
assert_equal __FILE__, location.path
205+
assert_equal @line, location.lineno
206+
end
207+
208+
test "returns the first clean location (:silent)" do
209+
location = invoke_first_clean_location(:silent)
210+
211+
assert_equal __FILE__, location.path
212+
assert_equal @line, location.lineno
213+
end
214+
215+
test "returns the first clean location (:noise)" do
216+
@bc.add_silencer { true }
217+
location = invoke_first_clean_location(:noise)
218+
219+
assert_equal __FILE__, location.path
220+
assert_equal @line, location.lineno
221+
end
222+
223+
test "returns the first clean location (:any)" do
224+
location = invoke_first_clean_location(:any) # fallback of the case statement
225+
226+
assert_equal __FILE__, location.path
227+
assert_equal @line, location.lineno
228+
end
229+
230+
test "returns nil if there is no clean location" do
231+
@bc.add_silencer { true }
232+
assert_nil invoke_first_clean_location_defaults
233+
end
234+
end

0 commit comments

Comments
 (0)