sentry-ruby 5.1.0 → 5.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +27 -0
  8. data/Makefile +4 -0
  9. data/README.md +8 -7
  10. data/Rakefile +13 -0
  11. data/bin/console +18 -0
  12. data/bin/setup +8 -0
  13. data/lib/sentry/background_worker.rb +72 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  16. data/lib/sentry/breadcrumb.rb +70 -0
  17. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  18. data/lib/sentry/client.rb +190 -0
  19. data/lib/sentry/configuration.rb +502 -0
  20. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  21. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  22. data/lib/sentry/dsn.rb +53 -0
  23. data/lib/sentry/envelope.rb +96 -0
  24. data/lib/sentry/error_event.rb +38 -0
  25. data/lib/sentry/event.rb +178 -0
  26. data/lib/sentry/exceptions.rb +9 -0
  27. data/lib/sentry/hub.rb +220 -0
  28. data/lib/sentry/integrable.rb +26 -0
  29. data/lib/sentry/interface.rb +16 -0
  30. data/lib/sentry/interfaces/exception.rb +43 -0
  31. data/lib/sentry/interfaces/request.rb +144 -0
  32. data/lib/sentry/interfaces/single_exception.rb +57 -0
  33. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  34. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  35. data/lib/sentry/interfaces/threads.rb +42 -0
  36. data/lib/sentry/linecache.rb +47 -0
  37. data/lib/sentry/logger.rb +20 -0
  38. data/lib/sentry/net/http.rb +115 -0
  39. data/lib/sentry/rack/capture_exceptions.rb +80 -0
  40. data/lib/sentry/rack.rb +5 -0
  41. data/lib/sentry/rake.rb +41 -0
  42. data/lib/sentry/redis.rb +90 -0
  43. data/lib/sentry/release_detector.rb +39 -0
  44. data/lib/sentry/scope.rb +295 -0
  45. data/lib/sentry/session.rb +35 -0
  46. data/lib/sentry/session_flusher.rb +90 -0
  47. data/lib/sentry/span.rb +226 -0
  48. data/lib/sentry/test_helper.rb +76 -0
  49. data/lib/sentry/transaction.rb +206 -0
  50. data/lib/sentry/transaction_event.rb +29 -0
  51. data/lib/sentry/transport/configuration.rb +25 -0
  52. data/lib/sentry/transport/dummy_transport.rb +21 -0
  53. data/lib/sentry/transport/http_transport.rb +175 -0
  54. data/lib/sentry/transport.rb +210 -0
  55. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  56. data/lib/sentry/utils/custom_inspection.rb +14 -0
  57. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  58. data/lib/sentry/utils/logging_helper.rb +26 -0
  59. data/lib/sentry/utils/real_ip.rb +84 -0
  60. data/lib/sentry/utils/request_id.rb +18 -0
  61. data/lib/sentry/version.rb +5 -0
  62. data/lib/sentry-ruby.rb +505 -0
  63. data/sentry-ruby-core.gemspec +23 -0
  64. data/sentry-ruby.gemspec +24 -0
  65. metadata +64 -16
@@ -0,0 +1,502 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/utility/processor_counter"
4
+
5
+ require "sentry/utils/exception_cause_chain"
6
+ require 'sentry/utils/custom_inspection'
7
+ require "sentry/dsn"
8
+ require "sentry/release_detector"
9
+ require "sentry/transport/configuration"
10
+ require "sentry/linecache"
11
+ require "sentry/interfaces/stacktrace_builder"
12
+
13
+ module Sentry
14
+ class Configuration
15
+ include CustomInspection
16
+ include LoggingHelper
17
+ # Directories to be recognized as part of your app. e.g. if you
18
+ # have an `engines` dir at the root of your project, you may want
19
+ # to set this to something like /(app|config|engines|lib)/
20
+ #
21
+ # @return [Regexp, nil]
22
+ attr_accessor :app_dirs_pattern
23
+
24
+ # Provide an object that responds to `call` to send events asynchronously.
25
+ # E.g.: lambda { |event| Thread.new { Sentry.send_event(event) } }
26
+ #
27
+ # @deprecated It will be removed in the next major release. Please read https://github.com/getsentry/sentry-ruby/issues/1522 for more information
28
+ # @return [Proc, nil]
29
+ attr_reader :async
30
+
31
+ # to send events in a non-blocking way, sentry-ruby has its own background worker
32
+ # by default, the worker holds a thread pool that has [the number of processors] threads
33
+ # but you can configure it with this configuration option
34
+ # E.g.: config.background_worker_threads = 5
35
+ #
36
+ # if you want to send events synchronously, set the value to 0
37
+ # E.g.: config.background_worker_threads = 0
38
+ # @return [Integer]
39
+ attr_accessor :background_worker_threads
40
+
41
+ # a proc/lambda that takes an array of stack traces
42
+ # it'll be used to silence (reduce) backtrace of the exception
43
+ #
44
+ # @example
45
+ # config.backtrace_cleanup_callback = lambda do |backtrace|
46
+ # Rails.backtrace_cleaner.clean(backtrace)
47
+ # end
48
+ #
49
+ # @return [Proc, nil]
50
+ attr_accessor :backtrace_cleanup_callback
51
+
52
+ # Optional Proc, called before adding the breadcrumb to the current scope
53
+ # @example
54
+ # config.before = lambda do |breadcrumb, hint|
55
+ # breadcrumb.message = 'a'
56
+ # breadcrumb
57
+ # end
58
+ # @return [Proc]
59
+ attr_reader :before_breadcrumb
60
+
61
+ # Optional Proc, called before sending an event to the server
62
+ # @example
63
+ # config.before_send = lambda do |event, hint|
64
+ # # skip ZeroDivisionError exceptions
65
+ # # note: hint[:exception] would be a String if you use async callback
66
+ # if hint[:exception].is_a?(ZeroDivisionError)
67
+ # nil
68
+ # else
69
+ # event
70
+ # end
71
+ # end
72
+ # @return [Proc]
73
+ attr_reader :before_send
74
+
75
+ # An array of breadcrumbs loggers to be used. Available options are:
76
+ # - :sentry_logger
77
+ # - :http_logger
78
+ # - :redis_logger
79
+ #
80
+ # And if you also use sentry-rails:
81
+ # - :active_support_logger
82
+ # - :monotonic_active_support_logger
83
+ #
84
+ # @return [Array<Symbol>]
85
+ attr_reader :breadcrumbs_logger
86
+
87
+ # Whether to capture local variables from the raised exception's frame. Default is false.
88
+ # @return [Boolean]
89
+ attr_accessor :capture_exception_frame_locals
90
+
91
+ # Max number of breadcrumbs a breadcrumb buffer can hold
92
+ # @return [Integer]
93
+ attr_accessor :max_breadcrumbs
94
+
95
+ # Number of lines of code context to capture, or nil for none
96
+ # @return [Integer, nil]
97
+ attr_accessor :context_lines
98
+
99
+ # RACK_ENV by default.
100
+ # @return [String]
101
+ attr_reader :environment
102
+
103
+ # Whether the SDK should run in the debugging mode. Default is false.
104
+ # If set to true, SDK errors will be logged with backtrace
105
+ # @return [Boolean]
106
+ attr_accessor :debug
107
+
108
+ # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
109
+ # @return [String]
110
+ attr_reader :dsn
111
+
112
+ # Whitelist of enabled_environments that will send notifications to Sentry. Array of Strings.
113
+ # @return [Array<String>]
114
+ attr_accessor :enabled_environments
115
+
116
+ # Logger 'progname's to exclude from breadcrumbs
117
+ # @return [Array<String>]
118
+ attr_accessor :exclude_loggers
119
+
120
+ # Array of exception classes that should never be sent. See IGNORE_DEFAULT.
121
+ # You should probably append to this rather than overwrite it.
122
+ # @return [Array<String>]
123
+ attr_accessor :excluded_exceptions
124
+
125
+ # Boolean to check nested exceptions when deciding if to exclude. Defaults to true
126
+ # @return [Boolean]
127
+ attr_accessor :inspect_exception_causes_for_exclusion
128
+ alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion
129
+
130
+ # You may provide your own LineCache for matching paths with source files.
131
+ # This may be useful if you need to get source code from places other than the disk.
132
+ # @see LineCache
133
+ # @return [LineCache]
134
+ attr_accessor :linecache
135
+
136
+ # Logger used by Sentry. In Rails, this is the Rails logger, otherwise
137
+ # Sentry provides its own Sentry::Logger.
138
+ # @return [Logger]
139
+ attr_accessor :logger
140
+
141
+ # Project directory root for in_app detection. Could be Rails root, etc.
142
+ # Set automatically for Rails.
143
+ # @return [String]
144
+ attr_accessor :project_root
145
+
146
+ # Insert sentry-trace to outgoing requests' headers
147
+ # @return [Boolean]
148
+ attr_accessor :propagate_traces
149
+
150
+ # Array of rack env parameters to be included in the event sent to sentry.
151
+ # @return [Array<String>]
152
+ attr_accessor :rack_env_whitelist
153
+
154
+ # Release tag to be passed with every event sent to Sentry.
155
+ # We automatically try to set this to a git SHA or Capistrano release.
156
+ # @return [String]
157
+ attr_accessor :release
158
+
159
+ # The sampling factor to apply to events. A value of 0.0 will not send
160
+ # any events, and a value of 1.0 will send 100% of events.
161
+ # @return [Float]
162
+ attr_accessor :sample_rate
163
+
164
+ # Include module versions in reports - boolean.
165
+ # @return [Boolean]
166
+ attr_accessor :send_modules
167
+
168
+ # When send_default_pii's value is false (default), sensitive information like
169
+ # - user ip
170
+ # - user cookie
171
+ # - request body
172
+ # - query string
173
+ # will not be sent to Sentry.
174
+ # @return [Boolean]
175
+ attr_accessor :send_default_pii
176
+
177
+ # Allow to skip Sentry emails within rake tasks
178
+ # @return [Boolean]
179
+ attr_accessor :skip_rake_integration
180
+
181
+ # IP ranges for trusted proxies that will be skipped when calculating IP address.
182
+ attr_accessor :trusted_proxies
183
+
184
+ # @return [String]
185
+ attr_accessor :server_name
186
+
187
+ # Return a Transport::Configuration object for transport-related configurations.
188
+ # @return [Transport]
189
+ attr_reader :transport
190
+
191
+ # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions).
192
+ # @return [Float]
193
+ attr_accessor :traces_sample_rate
194
+
195
+ # Take a Proc that controls the sample rate for every tracing event, e.g.
196
+ # @example
197
+ # config.traces_sampler = lambda do |tracing_context|
198
+ # # tracing_context[:transaction_context] contains the information about the transaction
199
+ # # tracing_context[:parent_sampled] contains the transaction's parent's sample decision
200
+ # true # return value can be a boolean or a float between 0.0 and 1.0
201
+ # end
202
+ # @return [Proc]
203
+ attr_accessor :traces_sampler
204
+
205
+ # Send diagnostic client reports about dropped events, true by default
206
+ # tries to attach to an existing envelope max once every 30s
207
+ # @return [Boolean]
208
+ attr_accessor :send_client_reports
209
+
210
+ # Track sessions in request/response cycles automatically
211
+ # @return [Boolean]
212
+ attr_accessor :auto_session_tracking
213
+
214
+ # these are not config options
215
+ # @!visibility private
216
+ attr_reader :errors, :gem_specs
217
+
218
+ # Most of these errors generate 4XX responses. In general, Sentry clients
219
+ # only automatically report 5xx responses.
220
+ IGNORE_DEFAULT = [
221
+ 'Mongoid::Errors::DocumentNotFound',
222
+ 'Rack::QueryParser::InvalidParameterError',
223
+ 'Rack::QueryParser::ParameterTypeError',
224
+ 'Sinatra::NotFound'
225
+ ].freeze
226
+
227
+ RACK_ENV_WHITELIST_DEFAULT = %w(
228
+ REMOTE_ADDR
229
+ SERVER_NAME
230
+ SERVER_PORT
231
+ ).freeze
232
+
233
+ HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\
234
+ "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`".freeze
235
+
236
+ LOG_PREFIX = "** [Sentry] ".freeze
237
+ MODULE_SEPARATOR = "::".freeze
238
+ SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder]
239
+
240
+ # Post initialization callbacks are called at the end of initialization process
241
+ # allowing extending the configuration of sentry-ruby by multiple extensions
242
+ @@post_initialization_callbacks = []
243
+
244
+ def initialize
245
+ self.app_dirs_pattern = nil
246
+ self.debug = false
247
+ self.background_worker_threads = Concurrent.processor_count
248
+ self.backtrace_cleanup_callback = nil
249
+ self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
250
+ self.breadcrumbs_logger = []
251
+ self.context_lines = 3
252
+ self.capture_exception_frame_locals = false
253
+ self.environment = environment_from_env
254
+ self.enabled_environments = []
255
+ self.exclude_loggers = []
256
+ self.excluded_exceptions = IGNORE_DEFAULT.dup
257
+ self.inspect_exception_causes_for_exclusion = true
258
+ self.linecache = ::Sentry::LineCache.new
259
+ self.logger = ::Sentry::Logger.new(STDOUT)
260
+ self.project_root = Dir.pwd
261
+ self.propagate_traces = true
262
+
263
+ self.sample_rate = 1.0
264
+ self.send_modules = true
265
+ self.send_default_pii = false
266
+ self.skip_rake_integration = false
267
+ self.send_client_reports = true
268
+ self.auto_session_tracking = true
269
+ self.trusted_proxies = []
270
+ self.dsn = ENV['SENTRY_DSN']
271
+ self.server_name = server_name_from_env
272
+
273
+ self.before_send = nil
274
+ self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
275
+ self.traces_sample_rate = nil
276
+ self.traces_sampler = nil
277
+
278
+ @transport = Transport::Configuration.new
279
+ @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
280
+
281
+ run_post_initialization_callbacks
282
+ end
283
+
284
+ def dsn=(value)
285
+ @dsn = init_dsn(value)
286
+ end
287
+
288
+ alias server= dsn=
289
+
290
+ def async=(value)
291
+ check_callable!("async", value)
292
+
293
+ log_warn <<~MSG
294
+
295
+ sentry-ruby now sends events asynchronously by default with its background worker (supported since 4.1.0).
296
+ The `config.async` callback has become redundant while continuing to cause issues.
297
+ (The problems of `async` are detailed in https://github.com/getsentry/sentry-ruby/issues/1522)
298
+
299
+ Therefore, we encourage you to remove it and let the background worker take care of async job sending.
300
+ It's deprecation is planned in the next major release (6.0), which is scheduled around the 3rd quarter of 2022.
301
+ MSG
302
+
303
+ @async = value
304
+ end
305
+
306
+ def breadcrumbs_logger=(logger)
307
+ loggers =
308
+ if logger.is_a?(Array)
309
+ logger
310
+ else
311
+ Array(logger)
312
+ end
313
+
314
+ require "sentry/breadcrumb/sentry_logger" if loggers.include?(:sentry_logger)
315
+
316
+ @breadcrumbs_logger = logger
317
+ end
318
+
319
+ def before_send=(value)
320
+ check_callable!("before_send", value)
321
+
322
+ @before_send = value
323
+ end
324
+
325
+ def before_breadcrumb=(value)
326
+ check_callable!("before_breadcrumb", value)
327
+
328
+ @before_breadcrumb = value
329
+ end
330
+
331
+ def environment=(environment)
332
+ @environment = environment.to_s
333
+ end
334
+
335
+ def sending_allowed?
336
+ @errors = []
337
+
338
+ valid? && capture_in_environment?
339
+ end
340
+
341
+ def sample_allowed?
342
+ return true if sample_rate == 1.0
343
+
344
+ Random.rand < sample_rate
345
+ end
346
+
347
+ def exception_class_allowed?(exc)
348
+ if exc.is_a?(Sentry::Error)
349
+ # Try to prevent error reporting loops
350
+ log_debug("Refusing to capture Sentry error: #{exc.inspect}")
351
+ false
352
+ elsif excluded_exception?(exc)
353
+ log_debug("User excluded error: #{exc.inspect}")
354
+ false
355
+ else
356
+ true
357
+ end
358
+ end
359
+
360
+ def enabled_in_current_env?
361
+ enabled_environments.empty? || enabled_environments.include?(environment)
362
+ end
363
+
364
+ def tracing_enabled?
365
+ !!((@traces_sample_rate && @traces_sample_rate >= 0.0 && @traces_sample_rate <= 1.0) || @traces_sampler) && sending_allowed?
366
+ end
367
+
368
+ # @return [String, nil]
369
+ def csp_report_uri
370
+ if dsn && dsn.valid?
371
+ uri = dsn.csp_report_uri
372
+ uri += "&sentry_release=#{CGI.escape(release)}" if release && !release.empty?
373
+ uri += "&sentry_environment=#{CGI.escape(environment)}" if environment && !environment.empty?
374
+ uri
375
+ end
376
+ end
377
+
378
+ # @api private
379
+ def stacktrace_builder
380
+ @stacktrace_builder ||= StacktraceBuilder.new(
381
+ project_root: @project_root.to_s,
382
+ app_dirs_pattern: @app_dirs_pattern,
383
+ linecache: @linecache,
384
+ context_lines: @context_lines,
385
+ backtrace_cleanup_callback: @backtrace_cleanup_callback
386
+ )
387
+ end
388
+
389
+ # @api private
390
+ def detect_release
391
+ return unless sending_allowed?
392
+
393
+ self.release ||= ReleaseDetector.detect_release(project_root: project_root, running_on_heroku: running_on_heroku?)
394
+
395
+ if running_on_heroku? && release.nil?
396
+ log_warn(HEROKU_DYNO_METADATA_MESSAGE)
397
+ end
398
+ rescue => e
399
+ log_error("Error detecting release", e, debug: debug)
400
+ end
401
+
402
+ # @api private
403
+ def error_messages
404
+ @errors = [@errors[0]] + @errors[1..-1].map(&:downcase) # fix case of all but first
405
+ @errors.join(", ")
406
+ end
407
+
408
+ private
409
+
410
+ def check_callable!(name, value)
411
+ unless value == nil || value.respond_to?(:call)
412
+ raise ArgumentError, "#{name} must be callable (or nil to disable)"
413
+ end
414
+ end
415
+
416
+ def init_dsn(dsn_string)
417
+ return if dsn_string.nil? || dsn_string.empty?
418
+
419
+ DSN.new(dsn_string)
420
+ end
421
+
422
+ def excluded_exception?(incoming_exception)
423
+ excluded_exception_classes.any? do |excluded_exception|
424
+ matches_exception?(excluded_exception, incoming_exception)
425
+ end
426
+ end
427
+
428
+ def excluded_exception_classes
429
+ @excluded_exception_classes ||= excluded_exceptions.map { |e| get_exception_class(e) }
430
+ end
431
+
432
+ def get_exception_class(x)
433
+ x.is_a?(Module) ? x : safe_const_get(x)
434
+ end
435
+
436
+ def matches_exception?(excluded_exception_class, incoming_exception)
437
+ if inspect_exception_causes_for_exclusion?
438
+ Sentry::Utils::ExceptionCauseChain.exception_to_array(incoming_exception).any? { |cause| excluded_exception_class === cause }
439
+ else
440
+ excluded_exception_class === incoming_exception
441
+ end
442
+ end
443
+
444
+ def safe_const_get(x)
445
+ x = x.to_s unless x.is_a?(String)
446
+ Object.const_get(x)
447
+ rescue NameError # There's no way to safely ask if a constant exist for an unknown string
448
+ nil
449
+ end
450
+
451
+ def capture_in_environment?
452
+ return true if enabled_in_current_env?
453
+
454
+ @errors << "Not configured to send/capture in environment '#{environment}'"
455
+ false
456
+ end
457
+
458
+ def valid?
459
+ if @dsn&.valid?
460
+ true
461
+ else
462
+ @errors << "DSN not set or not valid"
463
+ false
464
+ end
465
+ end
466
+
467
+ def environment_from_env
468
+ ENV['SENTRY_CURRENT_ENV'] || ENV['SENTRY_ENVIRONMENT'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
469
+ end
470
+
471
+ def server_name_from_env
472
+ if running_on_heroku?
473
+ ENV['DYNO']
474
+ else
475
+ # Try to resolve the hostname to an FQDN, but fall back to whatever
476
+ # the load name is.
477
+ Socket.gethostname || Socket.gethostbyname(hostname).first rescue server_name
478
+ end
479
+ end
480
+
481
+ def running_on_heroku?
482
+ File.directory?("/etc/heroku") && !ENV["CI"]
483
+ end
484
+
485
+ def run_post_initialization_callbacks
486
+ self.class.post_initialization_callbacks.each do |hook|
487
+ instance_eval(&hook)
488
+ end
489
+ end
490
+
491
+ # allow extensions to add their hooks to the Configuration class
492
+ def self.add_post_initialization_callback(&block)
493
+ self.post_initialization_callbacks << block
494
+ end
495
+
496
+ protected
497
+
498
+ def self.post_initialization_callbacks
499
+ @@post_initialization_callbacks
500
+ end
501
+ end
502
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ return if Object.method_defined?(:deep_dup)
4
+
5
+ require 'sentry/core_ext/object/duplicable'
6
+
7
+ #########################################
8
+ # This file was copied from Rails 5.2 #
9
+ #########################################
10
+
11
+ class Object
12
+ # Returns a deep copy of object if it's duplicable. If it's
13
+ # not duplicable, returns +self+.
14
+ #
15
+ # object = Object.new
16
+ # dup = object.deep_dup
17
+ # dup.instance_variable_set(:@a, 1)
18
+ #
19
+ # object.instance_variable_defined?(:@a) # => false
20
+ # dup.instance_variable_defined?(:@a) # => true
21
+ def deep_dup
22
+ duplicable? ? dup : self
23
+ end
24
+ end
25
+
26
+ class Array
27
+ # Returns a deep copy of array.
28
+ #
29
+ # array = [1, [2, 3]]
30
+ # dup = array.deep_dup
31
+ # dup[1][2] = 4
32
+ #
33
+ # array[1][2] # => nil
34
+ # dup[1][2] # => 4
35
+ def deep_dup
36
+ map(&:deep_dup)
37
+ end
38
+ end
39
+
40
+ class Hash
41
+ # Returns a deep copy of hash.
42
+ #
43
+ # hash = { a: { b: 'b' } }
44
+ # dup = hash.deep_dup
45
+ # dup[:a][:c] = 'c'
46
+ #
47
+ # hash[:a][:c] # => nil
48
+ # dup[:a][:c] # => "c"
49
+ def deep_dup
50
+ hash = dup
51
+ each_pair do |key, value|
52
+ if key.frozen? && ::String === key
53
+ hash[key] = value.deep_dup
54
+ else
55
+ hash.delete(key)
56
+ hash[key.deep_dup] = value.deep_dup
57
+ end
58
+ end
59
+ hash
60
+ end
61
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ return if Object.method_defined?(:duplicable?)
4
+
5
+ #########################################
6
+ # This file was copied from Rails 5.2 #
7
+ #########################################
8
+
9
+ #--
10
+ # Most objects are cloneable, but not all. For example you can't dup methods:
11
+ #
12
+ # method(:puts).dup # => TypeError: allocator undefined for Method
13
+ #
14
+ # Classes may signal their instances are not duplicable removing +dup+/+clone+
15
+ # or raising exceptions from them. So, to dup an arbitrary object you normally
16
+ # use an optimistic approach and are ready to catch an exception, say:
17
+ #
18
+ # arbitrary_object.dup rescue object
19
+ #
20
+ # Rails dups objects in a few critical spots where they are not that arbitrary.
21
+ # That rescue is very expensive (like 40 times slower than a predicate), and it
22
+ # is often triggered.
23
+ #
24
+ # That's why we hardcode the following cases and check duplicable? instead of
25
+ # using that rescue idiom.
26
+ #++
27
+ class Object
28
+ # Can you safely dup this object?
29
+ #
30
+ # False for method objects;
31
+ # true otherwise.
32
+ def duplicable?
33
+ true
34
+ end
35
+ end
36
+
37
+ class NilClass
38
+ begin
39
+ nil.dup
40
+ rescue TypeError
41
+ # +nil+ is not duplicable:
42
+ #
43
+ # nil.duplicable? # => false
44
+ # nil.dup # => TypeError: can't dup NilClass
45
+ def duplicable?
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+ class FalseClass
52
+ begin
53
+ false.dup
54
+ rescue TypeError
55
+ # +false+ is not duplicable:
56
+ #
57
+ # false.duplicable? # => false
58
+ # false.dup # => TypeError: can't dup FalseClass
59
+ def duplicable?
60
+ false
61
+ end
62
+ end
63
+ end
64
+
65
+ class TrueClass
66
+ begin
67
+ true.dup
68
+ rescue TypeError
69
+ # +true+ is not duplicable:
70
+ #
71
+ # true.duplicable? # => false
72
+ # true.dup # => TypeError: can't dup TrueClass
73
+ def duplicable?
74
+ false
75
+ end
76
+ end
77
+ end
78
+
79
+ class Symbol
80
+ begin
81
+ :symbol.dup # Ruby 2.4.x.
82
+ "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
83
+ rescue TypeError
84
+ # Symbols are not duplicable:
85
+ #
86
+ # :my_symbol.duplicable? # => false
87
+ # :my_symbol.dup # => TypeError: can't dup Symbol
88
+ def duplicable?
89
+ false
90
+ end
91
+ end
92
+ end
93
+
94
+ class Numeric
95
+ begin
96
+ 1.dup
97
+ rescue TypeError
98
+ # Numbers are not duplicable:
99
+ #
100
+ # 3.duplicable? # => false
101
+ # 3.dup # => TypeError: can't dup Integer
102
+ def duplicable?
103
+ false
104
+ end
105
+ end
106
+ end
107
+
108
+ require "bigdecimal"
109
+ class BigDecimal
110
+ # BigDecimals are duplicable:
111
+ #
112
+ # BigDecimal("1.2").duplicable? # => true
113
+ # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
114
+ def duplicable?
115
+ true
116
+ end
117
+ end
118
+
119
+ class Method
120
+ # Methods are not duplicable:
121
+ #
122
+ # method(:puts).duplicable? # => false
123
+ # method(:puts).dup # => TypeError: allocator undefined for Method
124
+ def duplicable?
125
+ false
126
+ end
127
+ end
128
+
129
+ class Complex
130
+ begin
131
+ Complex(1).dup
132
+ rescue TypeError
133
+ # Complexes are not duplicable:
134
+ #
135
+ # Complex(1).duplicable? # => false
136
+ # Complex(1).dup # => TypeError: can't copy Complex
137
+ def duplicable?
138
+ false
139
+ end
140
+ end
141
+ end
142
+
143
+ class Rational
144
+ begin
145
+ Rational(1).dup
146
+ rescue TypeError
147
+ # Rationals are not duplicable:
148
+ #
149
+ # Rational(1).duplicable? # => false
150
+ # Rational(1).dup # => TypeError: can't copy Rational
151
+ def duplicable?
152
+ false
153
+ end
154
+ end
155
+ end