activesupport 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +391 -67
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +72 -1
  5. data/lib/active_support/broadcast_logger.rb +61 -74
  6. data/lib/active_support/cache/file_store.rb +2 -2
  7. data/lib/active_support/cache/mem_cache_store.rb +26 -28
  8. data/lib/active_support/cache/memory_store.rb +5 -5
  9. data/lib/active_support/cache/null_store.rb +2 -2
  10. data/lib/active_support/cache/redis_cache_store.rb +37 -31
  11. data/lib/active_support/cache/strategy/local_cache.rb +72 -27
  12. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  13. data/lib/active_support/cache.rb +72 -9
  14. data/lib/active_support/callbacks.rb +27 -11
  15. data/lib/active_support/class_attribute.rb +15 -8
  16. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  17. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  18. data/lib/active_support/configurable.rb +34 -0
  19. data/lib/active_support/continuous_integration.rb +145 -0
  20. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  21. data/lib/active_support/core_ext/array.rb +7 -7
  22. data/lib/active_support/core_ext/benchmark.rb +4 -11
  23. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  24. data/lib/active_support/core_ext/class/attribute.rb +20 -5
  25. data/lib/active_support/core_ext/class.rb +2 -2
  26. data/lib/active_support/core_ext/date.rb +5 -5
  27. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  28. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  29. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  30. data/lib/active_support/core_ext/date_time.rb +5 -5
  31. data/lib/active_support/core_ext/digest.rb +1 -1
  32. data/lib/active_support/core_ext/enumerable.rb +17 -5
  33. data/lib/active_support/core_ext/erb/util.rb +5 -5
  34. data/lib/active_support/core_ext/file.rb +1 -1
  35. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  36. data/lib/active_support/core_ext/hash.rb +8 -8
  37. data/lib/active_support/core_ext/integer.rb +3 -3
  38. data/lib/active_support/core_ext/kernel.rb +3 -3
  39. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  40. data/lib/active_support/core_ext/module.rb +11 -11
  41. data/lib/active_support/core_ext/numeric.rb +3 -3
  42. data/lib/active_support/core_ext/object/json.rb +8 -1
  43. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  44. data/lib/active_support/core_ext/object/try.rb +2 -2
  45. data/lib/active_support/core_ext/object.rb +13 -13
  46. data/lib/active_support/core_ext/pathname.rb +2 -2
  47. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  48. data/lib/active_support/core_ext/range/sole.rb +17 -0
  49. data/lib/active_support/core_ext/range.rb +4 -4
  50. data/lib/active_support/core_ext/securerandom.rb +24 -8
  51. data/lib/active_support/core_ext/string/filters.rb +3 -3
  52. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  53. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  54. data/lib/active_support/core_ext/string.rb +13 -13
  55. data/lib/active_support/core_ext/symbol.rb +1 -1
  56. data/lib/active_support/core_ext/time/calculations.rb +0 -7
  57. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  58. data/lib/active_support/core_ext/time.rb +5 -5
  59. data/lib/active_support/core_ext.rb +1 -1
  60. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  61. data/lib/active_support/current_attributes.rb +27 -17
  62. data/lib/active_support/dependencies/interlock.rb +11 -5
  63. data/lib/active_support/dependencies.rb +6 -1
  64. data/lib/active_support/deprecation/reporting.rb +4 -2
  65. data/lib/active_support/deprecation.rb +1 -1
  66. data/lib/active_support/editor.rb +70 -0
  67. data/lib/active_support/error_reporter.rb +61 -8
  68. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  69. data/lib/active_support/event_reporter.rb +592 -0
  70. data/lib/active_support/evented_file_update_checker.rb +5 -1
  71. data/lib/active_support/execution_context.rb +64 -7
  72. data/lib/active_support/execution_wrapper.rb +1 -1
  73. data/lib/active_support/file_update_checker.rb +8 -6
  74. data/lib/active_support/gem_version.rb +1 -1
  75. data/lib/active_support/gzip.rb +1 -0
  76. data/lib/active_support/hash_with_indifferent_access.rb +47 -24
  77. data/lib/active_support/i18n_railtie.rb +1 -2
  78. data/lib/active_support/inflector/inflections.rb +31 -15
  79. data/lib/active_support/inflector/transliterate.rb +6 -8
  80. data/lib/active_support/isolated_execution_state.rb +12 -15
  81. data/lib/active_support/json/decoding.rb +6 -4
  82. data/lib/active_support/json/encoding.rb +155 -19
  83. data/lib/active_support/lazy_load_hooks.rb +1 -1
  84. data/lib/active_support/log_subscriber.rb +2 -6
  85. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  86. data/lib/active_support/message_encryptors.rb +54 -2
  87. data/lib/active_support/message_pack/extensions.rb +5 -0
  88. data/lib/active_support/message_verifier.rb +9 -0
  89. data/lib/active_support/message_verifiers.rb +57 -3
  90. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  91. data/lib/active_support/messages/rotator.rb +10 -0
  92. data/lib/active_support/multibyte/chars.rb +12 -2
  93. data/lib/active_support/multibyte.rb +4 -0
  94. data/lib/active_support/notifications/fanout.rb +64 -42
  95. data/lib/active_support/notifications/instrumenter.rb +1 -1
  96. data/lib/active_support/railtie.rb +32 -13
  97. data/lib/active_support/structured_event_subscriber.rb +99 -0
  98. data/lib/active_support/subscriber.rb +0 -5
  99. data/lib/active_support/syntax_error_proxy.rb +3 -0
  100. data/lib/active_support/test_case.rb +61 -6
  101. data/lib/active_support/testing/assertions.rb +34 -6
  102. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  103. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  104. data/lib/active_support/testing/notification_assertions.rb +92 -0
  105. data/lib/active_support/testing/parallelization/server.rb +15 -2
  106. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  107. data/lib/active_support/testing/parallelization.rb +25 -1
  108. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  109. data/lib/active_support/testing/time_helpers.rb +7 -3
  110. data/lib/active_support/time_with_zone.rb +23 -23
  111. data/lib/active_support/values/time_zone.rb +8 -1
  112. data/lib/active_support/xml_mini.rb +3 -2
  113. data/lib/active_support.rb +20 -15
  114. metadata +27 -23
  115. data/lib/active_support/core_ext/range/each.rb +0 -24
  116. data/lib/active_support/testing/strict_warnings.rb +0 -43
@@ -67,14 +67,13 @@ module ActiveSupport # :nodoc:
67
67
  original_concat(value)
68
68
  end
69
69
 
70
- def initialize(str = "")
71
- @html_safe = true
70
+ def initialize(_str = "")
72
71
  super
73
72
  end
74
73
 
75
74
  def initialize_copy(other)
76
75
  super
77
- @html_safe = other.html_safe?
76
+ @html_unsafe = true unless other.html_safe?
78
77
  end
79
78
 
80
79
  def concat(value)
@@ -116,7 +115,9 @@ module ActiveSupport # :nodoc:
116
115
  def *(_)
117
116
  new_string = super
118
117
  new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
119
- new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
118
+ if @html_unsafe
119
+ new_safe_buffer.instance_variable_set(:@html_unsafe, true)
120
+ end
120
121
  new_safe_buffer
121
122
  end
122
123
 
@@ -131,14 +132,18 @@ module ActiveSupport # :nodoc:
131
132
  self.class.new(super(escaped_args))
132
133
  end
133
134
 
134
- attr_reader :html_safe
135
- alias_method :html_safe?, :html_safe
136
- remove_method :html_safe
135
+ def html_safe?
136
+ @html_unsafe.nil?
137
+ end
137
138
 
138
139
  def to_s
139
140
  self
140
141
  end
141
142
 
143
+ def as_json(*)
144
+ to_str
145
+ end
146
+
142
147
  def to_param
143
148
  to_str
144
149
  end
@@ -155,7 +160,7 @@ module ActiveSupport # :nodoc:
155
160
  end # end
156
161
 
157
162
  def #{unsafe_method}!(*args) # def capitalize!(*args)
158
- @html_safe = false # @html_safe = false
163
+ @html_unsafe = true # @html_unsafe = true
159
164
  super # super
160
165
  end # end
161
166
  EOT
@@ -176,7 +181,7 @@ module ActiveSupport # :nodoc:
176
181
  end # end
177
182
 
178
183
  def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
179
- @html_safe = false # @html_safe = false
184
+ @html_unsafe = true # @html_unsafe = true
180
185
  if block # if block
181
186
  super(*args) { |*params| # super(*args) { |*params|
182
187
  set_block_back_references(block, $~) # set_block_back_references(block, $~)
@@ -191,14 +196,14 @@ module ActiveSupport # :nodoc:
191
196
 
192
197
  private
193
198
  def explicit_html_escape_interpolated_argument(arg)
194
- (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
199
+ (!html_safe? || arg.html_safe?) ? arg : ERB::Util.unwrapped_html_escape(arg)
195
200
  end
196
201
 
197
202
  def implicit_html_escape_interpolated_argument(arg)
198
203
  if !html_safe? || arg.html_safe?
199
204
  arg
200
205
  else
201
- CGI.escapeHTML(arg.to_str)
206
+ ERB::Util.unwrapped_html_escape(arg.to_str)
202
207
  end
203
208
  end
204
209
 
@@ -210,7 +215,9 @@ module ActiveSupport # :nodoc:
210
215
 
211
216
  def string_into_safe_buffer(new_string, is_html_safe)
212
217
  new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
213
- new_safe_buffer.instance_variable_set :@html_safe, is_html_safe
218
+ unless is_html_safe
219
+ new_safe_buffer.instance_variable_set :@html_unsafe, true
220
+ end
214
221
  new_safe_buffer
215
222
  end
216
223
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string/conversions"
4
- require "active_support/core_ext/string/filters"
5
- require "active_support/core_ext/string/multibyte"
6
- require "active_support/core_ext/string/starts_ends_with"
7
- require "active_support/core_ext/string/inflections"
8
- require "active_support/core_ext/string/access"
9
- require "active_support/core_ext/string/behavior"
10
- require "active_support/core_ext/string/output_safety"
11
- require "active_support/core_ext/string/exclude"
12
- require "active_support/core_ext/string/strip"
13
- require "active_support/core_ext/string/inquiry"
14
- require "active_support/core_ext/string/indent"
15
- require "active_support/core_ext/string/zones"
3
+ require_relative "string/conversions"
4
+ require_relative "string/filters"
5
+ require_relative "string/multibyte"
6
+ require_relative "string/starts_ends_with"
7
+ require_relative "string/inflections"
8
+ require_relative "string/access"
9
+ require_relative "string/behavior"
10
+ require_relative "string/output_safety"
11
+ require_relative "string/exclude"
12
+ require_relative "string/strip"
13
+ require_relative "string/inquiry"
14
+ require_relative "string/indent"
15
+ require_relative "string/zones"
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/symbol/starts_ends_with"
3
+ require_relative "symbol/starts_ends_with"
@@ -224,13 +224,6 @@ class Time
224
224
  # Returns a new Time representing the time a number of seconds since the instance time
225
225
  def since(seconds)
226
226
  self + seconds
227
- rescue TypeError
228
- result = to_datetime.since(seconds)
229
- ActiveSupport.deprecator.warn(
230
- "Passing an instance of #{seconds.class} to #{self.class}#since is deprecated. This behavior will raise " \
231
- "a `TypeError` in Rails 8.1."
232
- )
233
- result
234
227
  end
235
228
  alias :in :since
236
229
 
@@ -8,33 +8,8 @@ class Time
8
8
 
9
9
  silence_redefinition_of_method :to_time
10
10
 
11
- # Either return +self+ or the time in the local system timezone depending
12
- # on the setting of +ActiveSupport.to_time_preserves_timezone+.
11
+ # Return +self+.
13
12
  def to_time
14
- preserve_timezone ? self : getlocal
13
+ self
15
14
  end
16
-
17
- def preserve_timezone # :nodoc:
18
- system_local_time? || super
19
- end
20
-
21
- private
22
- def system_local_time?
23
- if ::Time.equal?(self.class)
24
- zone = self.zone
25
- String === zone &&
26
- (zone != "UTC" || active_support_local_zone == "UTC")
27
- end
28
- end
29
-
30
- @@active_support_local_tz = nil
31
-
32
- def active_support_local_zone
33
- @@active_support_local_zone = nil if @@active_support_local_tz != ENV["TZ"]
34
- @@active_support_local_zone ||=
35
- begin
36
- @@active_support_local_tz = ENV["TZ"]
37
- Time.new.zone
38
- end
39
- end
40
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/time/acts_like"
4
- require "active_support/core_ext/time/calculations"
5
- require "active_support/core_ext/time/compatibility"
6
- require "active_support/core_ext/time/conversions"
7
- require "active_support/core_ext/time/zones"
3
+ require_relative "time/acts_like"
4
+ require_relative "time/calculations"
5
+ require_relative "time/compatibility"
6
+ require_relative "time/conversions"
7
+ require_relative "time/zones"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort.each do |path|
3
+ (Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort - [File.expand_path("core_ext/benchmark.rb", __dir__)]).each do |path|
4
4
  require path
5
5
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  module ActiveSupport::CurrentAttributes::TestHelper # :nodoc:
4
4
  def before_setup
5
- ActiveSupport::CurrentAttributes.reset_all
5
+ ActiveSupport::CurrentAttributes.clear_all
6
6
  super
7
7
  end
8
8
 
9
9
  def after_teardown
10
10
  super
11
- ActiveSupport::CurrentAttributes.reset_all
11
+ ActiveSupport::CurrentAttributes.clear_all
12
12
  end
13
13
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/callbacks"
4
+ require "active_support/execution_context"
4
5
  require "active_support/core_ext/object/with"
5
6
  require "active_support/core_ext/enumerable"
6
7
  require "active_support/core_ext/module/delegation"
@@ -108,35 +109,35 @@ module ActiveSupport
108
109
  # ==== Options
109
110
  #
110
111
  # * <tt>:default</tt> - The default value for the attributes. If the value
111
- # is a proc or lambda, it will be called whenever an instance is
112
- # constructed. Otherwise, the value will be duplicated with +#dup+.
113
- # Default values are re-assigned when the attributes are reset.
112
+ # is a proc or lambda, it will be called whenever an instance is
113
+ # constructed. Otherwise, the value will be duplicated with +#dup+.
114
+ # Default values are re-assigned when the attributes are reset.
114
115
  def attribute(*names, default: NOT_SET)
115
116
  invalid_attribute_names = names.map(&:to_sym) & INVALID_ATTRIBUTE_NAMES
116
117
  if invalid_attribute_names.any?
117
118
  raise ArgumentError, "Restricted attribute names: #{invalid_attribute_names.join(", ")}"
118
119
  end
119
120
 
121
+ Delegation.generate(singleton_class, names, to: :instance, nilable: false, signature: "")
122
+ Delegation.generate(singleton_class, names.map { |n| "#{n}=" }, to: :instance, nilable: false, signature: "value")
123
+
120
124
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
121
125
  names.each do |name|
122
126
  owner.define_cached_method(name, namespace: :current_attributes) do |batch|
123
127
  batch <<
124
128
  "def #{name}" <<
125
- "attributes[:#{name}]" <<
129
+ "@attributes[:#{name}]" <<
126
130
  "end"
127
131
  end
128
132
  owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
129
133
  batch <<
130
134
  "def #{name}=(value)" <<
131
- "attributes[:#{name}] = value" <<
135
+ "@attributes[:#{name}] = value" <<
132
136
  "end"
133
137
  end
134
138
  end
135
139
  end
136
140
 
137
- Delegation.generate(singleton_class, names, to: :instance, nilable: false, signature: "")
138
- Delegation.generate(singleton_class, names.map { |n| "#{n}=" }, to: :instance, nilable: false, signature: "value")
139
-
140
141
  self.defaults = defaults.merge(names.index_with { default })
141
142
  end
142
143
 
@@ -153,13 +154,11 @@ module ActiveSupport
153
154
 
154
155
  delegate :set, :reset, to: :instance
155
156
 
156
- def reset_all # :nodoc:
157
- current_instances.each_value(&:reset)
158
- end
159
-
160
157
  def clear_all # :nodoc:
161
- reset_all
162
- current_instances.clear
158
+ if instances = current_instances
159
+ instances.values.each(&:reset)
160
+ instances.clear
161
+ end
163
162
  end
164
163
 
165
164
  private
@@ -168,7 +167,7 @@ module ActiveSupport
168
167
  end
169
168
 
170
169
  def current_instances
171
- IsolatedExecutionState[:current_attributes_instances] ||= {}
170
+ ExecutionContext.current_attributes_instances
172
171
  end
173
172
 
174
173
  def current_instances_key
@@ -185,21 +184,32 @@ module ActiveSupport
185
184
 
186
185
  def method_added(name)
187
186
  super
187
+
188
+ # We try to generate instance delegators early to not rely on method_missing.
188
189
  return if name == :initialize
190
+
191
+ # If the added method isn't public, we don't delegate it.
189
192
  return unless public_method_defined?(name)
190
- return if respond_to?(name, true)
193
+
194
+ # If we already have a class method by that name, we don't override it.
195
+ return if singleton_class.method_defined?(name) || singleton_class.private_method_defined?(name)
196
+
191
197
  Delegation.generate(singleton_class, [name], to: :instance, as: self, nilable: false)
192
198
  end
193
199
  end
194
200
 
195
201
  class_attribute :defaults, instance_writer: false, default: {}.freeze
196
202
 
197
- attr_accessor :attributes
203
+ attr_writer :attributes
198
204
 
199
205
  def initialize
200
206
  @attributes = resolve_defaults
201
207
  end
202
208
 
209
+ def attributes
210
+ @attributes.dup
211
+ end
212
+
203
213
  # Expose one or more attributes within a block. Old values are returned after the block concludes.
204
214
  # Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
205
215
  #
@@ -10,19 +10,24 @@ module ActiveSupport # :nodoc:
10
10
  end
11
11
 
12
12
  def loading(&block)
13
- @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load], &block)
13
+ ActiveSupport.deprecator.warn(
14
+ "ActiveSupport::Dependencies::Interlock#loading is deprecated and " \
15
+ "will be removed in Rails 9.0. The loading interlock is no longer " \
16
+ "used since Rails switched to Zeitwerk for autoloading."
17
+ )
18
+ yield if block
14
19
  end
15
20
 
16
21
  def unloading(&block)
17
- @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload], &block)
22
+ @lock.exclusive(purpose: :unload, compatible: [:unload], after_compatible: [:unload], &block)
18
23
  end
19
24
 
20
25
  def start_unloading
21
- @lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
26
+ @lock.start_exclusive(purpose: :unload, compatible: [:unload])
22
27
  end
23
28
 
24
29
  def done_unloading
25
- @lock.stop_exclusive(compatible: [:load, :unload])
30
+ @lock.stop_exclusive(compatible: [:unload])
26
31
  end
27
32
 
28
33
  def start_running
@@ -38,7 +43,8 @@ module ActiveSupport # :nodoc:
38
43
  end
39
44
 
40
45
  def permit_concurrent_loads(&block)
41
- @lock.yield_shares(compatible: [:load], &block)
46
+ # Soft deprecated: no deprecation warning for now, but this is a no-op.
47
+ yield if block
42
48
  end
43
49
 
44
50
  def raw_state(&block) # :nodoc:
@@ -21,7 +21,12 @@ module ActiveSupport # :nodoc:
21
21
  # preventing any other thread from being inside a #run_interlock
22
22
  # block at the same time.
23
23
  def self.load_interlock(&block)
24
- interlock.loading(&block)
24
+ ActiveSupport.deprecator.warn(
25
+ "ActiveSupport::Dependencies.load_interlock is deprecated and " \
26
+ "will be removed in Rails 9.0. The loading interlock is no longer " \
27
+ "used since Rails switched to Zeitwerk for autoloading."
28
+ )
29
+ yield if block
25
30
  end
26
31
 
27
32
  # Execute the supplied block while holding an exclusive lock,
@@ -149,8 +149,10 @@ module ActiveSupport
149
149
  [offending_line.path, offending_line.lineno, offending_line.label]
150
150
  end
151
151
 
152
- RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/" # :nodoc:
153
- LIB_DIR = RbConfig::CONFIG["libdir"] # :nodoc:
152
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
153
+ private_constant :RAILS_GEM_ROOT
154
+ LIB_DIR = RbConfig::CONFIG["libdir"]
155
+ private_constant :LIB_DIR
154
156
 
155
157
  def ignored_callstack?(path)
156
158
  path.start_with?(RAILS_GEM_ROOT, LIB_DIR) || path.include?("<internal:")
@@ -68,7 +68,7 @@ module ActiveSupport
68
68
  # and the second is a library name.
69
69
  #
70
70
  # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
71
- def initialize(deprecation_horizon = "8.1", gem_name = "Rails")
71
+ def initialize(deprecation_horizon = "8.2", gem_name = "Rails")
72
72
  self.gem_name = gem_name
73
73
  self.deprecation_horizon = deprecation_horizon
74
74
  # By default, warnings are not silenced and debugging is off.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActiveSupport
6
+ class Editor
7
+ @editors = {}
8
+ @current = false
9
+
10
+ class << self
11
+ # Registers a URL pattern for opening file in a given editor.
12
+ # This allows Rails to generate clickable links to control known editors.
13
+ #
14
+ # Example:
15
+ #
16
+ # ActiveSupport::Editor.register("myeditor", "myeditor://%s:%d")
17
+ def register(name, url_pattern, aliases: [])
18
+ editor = new(url_pattern)
19
+ @editors[name] = editor
20
+ aliases.each do |a|
21
+ @editors[a] = editor
22
+ end
23
+ end
24
+
25
+ # Returns the current editor pattern if it is known.
26
+ # First check for the `RAILS_EDITOR` environment variable, and if it's
27
+ # missing, check for the `EDITOR` environment variable.
28
+ def current
29
+ if @current == false
30
+ @current = if editor_name = ENV["RAILS_EDITOR"] || ENV["EDITOR"]
31
+ @editors[editor_name]
32
+ end
33
+ end
34
+ @current
35
+ end
36
+
37
+ # :nodoc:
38
+
39
+ def find(name)
40
+ @editors[name]
41
+ end
42
+
43
+ def reset
44
+ @current = false
45
+ end
46
+ end
47
+
48
+ def initialize(url_pattern)
49
+ @url_pattern = url_pattern
50
+ end
51
+
52
+ def url_for(path, line)
53
+ sprintf(@url_pattern, path, line)
54
+ end
55
+
56
+ register "atom", "atom://core/open/file?filename=%s&line=%d"
57
+ register "cursor", "cursor://file/%s:%f"
58
+ register "emacs", "emacs://open?url=file://%s&line=%d", aliases: ["emacsclient"]
59
+ register "idea", "idea://open?file=%s&line=%d"
60
+ register "macvim", "mvim://open?url=file://%s&line=%d", aliases: ["mvim"]
61
+ register "nova", "nova://open?path=%s&line=%d"
62
+ register "rubymine", "x-mine://open?file=%s&line=%d"
63
+ register "sublime", "subl://open?url=file://%s&line=%d", aliases: ["subl"]
64
+ register "textmate", "txmt://open?url=file://%s&line=%d", aliases: ["mate"]
65
+ register "vscode", "vscode://file/%s:%d", aliases: ["code"]
66
+ register "vscodium", "vscodium://file/%s:%d", aliases: ["codium"]
67
+ register "windsurf", "windsurf://file/%s:%d"
68
+ register "zed", "zed://file/%s:%d"
69
+ end
70
+ end
@@ -36,6 +36,7 @@ module ActiveSupport
36
36
  @subscribers = subscribers.flatten
37
37
  @logger = logger
38
38
  @debug_mode = false
39
+ @context_middlewares = ErrorContextMiddlewareStack.new
39
40
  end
40
41
 
41
42
  # Evaluates the given block, reporting and swallowing any unhandled error.
@@ -202,20 +203,51 @@ module ActiveSupport
202
203
  ActiveSupport::ExecutionContext.set(...)
203
204
  end
204
205
 
206
+ # Add a middleware to modify the error context before it is sent to subscribers.
207
+ #
208
+ # Middleware is added to a stack of callables run on an error's execution context
209
+ # before passing to subscribers. Allows creation of entries in error context that
210
+ # are shared by all subscribers.
211
+ #
212
+ # A context middleware receives the same parameters as #report.
213
+ # It must return a hash - the middleware stack returns the hash after it has
214
+ # run through all middlewares. A middleware can mutate or replace the hash.
215
+ #
216
+ # Rails.error.add_middleware(-> (error, context) { context.merge({ foo: :bar }) })
217
+ #
218
+ def add_middleware(middleware)
219
+ @context_middlewares.use(middleware)
220
+ end
221
+
205
222
  # Report an error directly to subscribers. You can use this method when the
206
223
  # block-based #handle and #record methods are not suitable.
207
224
  #
208
225
  # Rails.error.report(error)
209
226
  #
227
+ # The +error+ argument must be an instance of Exception.
228
+ #
229
+ # Rails.error.report(Exception.new("Something went wrong"))
230
+ #
231
+ # Otherwise you can use #unexpected to report an error which does accept a
232
+ # string argument.
210
233
  def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
211
234
  return if error.instance_variable_defined?(:@__rails_error_reported)
235
+ raise ArgumentError, "Reported error must be an Exception, got: #{error.inspect}" unless error.is_a?(Exception)
236
+
212
237
  ensure_backtrace(error)
213
238
 
214
239
  unless SEVERITIES.include?(severity)
215
240
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
216
241
  end
217
242
 
218
- full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
243
+ full_context = @context_middlewares.execute(
244
+ error,
245
+ context: ActiveSupport::ExecutionContext.to_h.merge(context || {}),
246
+ handled:,
247
+ severity:,
248
+ source:
249
+ )
250
+
219
251
  disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
220
252
  @subscribers.each do |subscriber|
221
253
  unless disabled_subscribers&.any? { |s| s === subscriber }
@@ -232,8 +264,11 @@ module ActiveSupport
232
264
  end
233
265
  end
234
266
 
235
- unless error.frozen?
236
- error.instance_variable_set(:@__rails_error_reported, true)
267
+ while error
268
+ unless error.frozen?
269
+ error.instance_variable_set(:@__rails_error_reported, true)
270
+ end
271
+ error = error.cause
237
272
  end
238
273
 
239
274
  nil
@@ -241,14 +276,12 @@ module ActiveSupport
241
276
 
242
277
  private
243
278
  def ensure_backtrace(error)
244
- return if error.frozen? # re-raising won't add a backtrace
279
+ return if error.frozen? # re-raising won't add a backtrace or set the cause
245
280
  return unless error.backtrace.nil?
246
281
 
247
282
  begin
248
- # We could use Exception#set_backtrace, but until Ruby 3.4
249
- # it only support setting `Exception#backtrace` and not
250
- # `Exception#backtrace_locations`. So raising the exception
251
- # is a good way to build a real backtrace.
283
+ # As of Ruby 3.4, we could use Exception#set_backtrace to set the backtrace,
284
+ # but there's nothing like Exception#set_cause. Raising+rescuing is the only way to set the cause.
252
285
  raise error
253
286
  rescue error.class => error
254
287
  end
@@ -261,5 +294,25 @@ module ActiveSupport
261
294
 
262
295
  error.backtrace.shift(count)
263
296
  end
297
+
298
+ class ErrorContextMiddlewareStack # :nodoc:
299
+ def initialize
300
+ @stack = []
301
+ end
302
+
303
+ # Add a middleware to the error context stack.
304
+ def use(middleware)
305
+ unless middleware.respond_to?(:call)
306
+ raise ArgumentError, "Error context middleware must respond to #call"
307
+ end
308
+
309
+ @stack << middleware
310
+ end
311
+
312
+ # Run all middlewares in the stack
313
+ def execute(error, handled:, severity:, context:, source:)
314
+ @stack.inject(context) { |c, middleware| middleware.call(error, context: c, handled:, severity:, source:) }
315
+ end
316
+ end
264
317
  end
265
318
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::EventReporter::TestHelper # :nodoc:
4
+ class EventSubscriber # :nodoc:
5
+ attr_reader :events
6
+
7
+ def initialize
8
+ @events = []
9
+ end
10
+
11
+ def emit(event)
12
+ @events << event
13
+ end
14
+ end
15
+
16
+ def event_matcher(name:, payload: nil, tags: {}, context: {}, source_location: nil)
17
+ ->(event) {
18
+ return false unless event[:name] == name
19
+ return false unless event[:payload] == payload
20
+ return false unless event[:tags] == tags
21
+ return false unless event[:context] == context
22
+
23
+ [:filepath, :lineno, :label].each do |key|
24
+ if source_location && source_location[key]
25
+ return false unless event[:source_location][key] == source_location[key]
26
+ end
27
+ end
28
+
29
+ true
30
+ }
31
+ end
32
+ end