enhanced_errors 0.1.7 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12d823c10e0bd52d8ca38f39e5851104676be83be50ee5d3246b339eea00b3d4
4
- data.tar.gz: 3d2df93d9c9b9712fe62bd1b3909032ece37ed8dc32ac9453d3b751655f6df83
3
+ metadata.gz: 42e0ffde5da4be326f8b1d721652eed031f7bfb20a07d31a256d69c588a383b7
4
+ data.tar.gz: 5ef8303d0e6dd089253643b2abbf4aeef9a195831c946b2c92396390685e0a5b
5
5
  SHA512:
6
- metadata.gz: 4952714cb57a30e8100889c7e275d96626abdb41b9bae9a894f4e8eb9d74de429d0bc9d3e1b8848cc89759d017258044c71014453186ffde80d5b19b90d1ce54
7
- data.tar.gz: 152b2feb1962389acf32e13ad965bb0959f5aed731840cf96979c840a2dd1e9843a599821b5b54acf599bf74cf0c4a9f90e21c34e6837aab412826a6662d072c
6
+ metadata.gz: cb553e2a7f127a2d7efbf77c25867a9706d500ffb70e6e48060cd42390cdd903f140d60a899ed61f0d3340697b3ec4458a704545aa4380dad928d64a76d9b158
7
+ data.tar.gz: 467e3f82cb5a170081b43f417dfa0b53c12df77a6a2614d13377d47227a00df0623d515a92707b52fd5cc8461f3862c16ae83c3aa264beff2ee63f88072299ce
data/README.md CHANGED
@@ -384,7 +384,9 @@ Ruby's TracePoint binding capture very narrowly with no other C API or dependenc
384
384
 
385
385
  - **TBD**: Memory considerations. This does capture data when an exception happens. EnhancedErrors hides under the bed when it sees **NoMemoryError**.
386
386
 
387
- - **Goal: Production Safety**: The gem is designed to, once vetted, be safe for production use, giving you valuable insights without compromising performance. I suggest letting it get well-vetted before making the leap and testing it for both performance and memory under load internally, as well.
387
+ - **Goal: Production Safety**: The gem is designed to, once vetted, be safe for production use, giving you valuable insights without compromising performance.
388
+ I suggest letting it get well-vetted before making the leap and testing it for both performance and memory under load internally, as well.
389
+ I would not enable it in production *yet*.
388
390
 
389
391
  ## Contributing
390
392
 
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "enhanced_errors"
3
- spec.version = "0.1.7"
3
+ spec.version = "1.0.0"
4
4
  spec.authors = ["Eric Beland"]
5
5
 
6
6
  spec.summary = "Automatically enhance your errors with messages containing variable values from the moment they were raised."
data/lib/binding.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # binding.rb
2
+
1
3
  module Debugging
2
4
  def let_vars_hash
3
5
  memoized_values = self.receiver.instance_variable_get(:@__memoized)&.instance_variable_get(:@memoized)
@@ -8,4 +10,3 @@ end
8
10
  class Binding
9
11
  include Debugging
10
12
  end
11
-
@@ -1,3 +1,5 @@
1
+ # enhanced_errors.rb
2
+
1
3
  require 'set'
2
4
  require 'json'
3
5
 
@@ -5,19 +7,27 @@ require_relative 'colors'
5
7
  require_relative 'error_enhancements'
6
8
  require_relative 'binding'
7
9
 
8
- # While we could just catch StandardError, we would miss a number of things.
10
+ # Exception class names to ignore. Using strings to avoid uninitialized constant errors.
11
+ IGNORED_EXCEPTION_NAMES = %w[SystemExit NoMemoryError SignalException Interrupt
12
+ ScriptError LoadError NotImplementedError SyntaxError
13
+ SystemStackError Psych::BadAlias]
14
+
15
+ # Helper method to safely resolve class names to constants
16
+ def resolve_exception_class(name)
17
+ names = name.split('::')
18
+ names.inject(Object) do |mod, name_part|
19
+ if mod.const_defined?(name_part, false)
20
+ mod.const_get(name_part)
21
+ else
22
+ return nil
23
+ end
24
+ end
25
+ rescue NameError
26
+ nil
27
+ end
9
28
 
10
- IGNORED_EXCEPTIONS = [
11
- SystemExit,
12
- NoMemoryError,
13
- SignalException,
14
- Interrupt,
15
- ScriptError,
16
- LoadError,
17
- NotImplementedError,
18
- SyntaxError,
19
- SystemStackError
20
- ]
29
+ # Attempt to resolve the exception classes, ignoring any that are not defined
30
+ IGNORED_EXCEPTIONS = IGNORED_EXCEPTION_NAMES.map { |name| resolve_exception_class(name) }.compact
21
31
 
22
32
  # The EnhancedErrors class provides mechanisms to enhance exception handling by capturing
23
33
  # additional context such as binding information, variables, and method arguments when exceptions are raised.
@@ -29,10 +39,10 @@ class EnhancedErrors
29
39
  # @return [Boolean]
30
40
  attr_accessor :enabled
31
41
 
32
- # The TracePoint object used for tracing exceptions.
42
+ # The TracePoint objects used for tracing exceptions per thread.
33
43
  #
34
- # @return [TracePoint, nil]
35
- attr_accessor :trace
44
+ # @return [Hash{Thread => TracePoint}]
45
+ attr_accessor :traces
36
46
 
37
47
  # The configuration block provided during enhancement.
38
48
  #
@@ -64,6 +74,11 @@ class EnhancedErrors
64
74
  # @return [Set<Symbol>]
65
75
  attr_accessor :skip_list
66
76
 
77
+ # Determines whether to capture :rescue events.
78
+ #
79
+ # @return [Boolean]
80
+ attr_accessor :capture_rescue
81
+
67
82
  # Regular expression to identify gem paths.
68
83
  #
69
84
  # @return [Regexp]
@@ -137,6 +152,19 @@ class EnhancedErrors
137
152
  @capture_let_variables
138
153
  end
139
154
 
155
+ # Gets or sets whether to capture :rescue events.
156
+ #
157
+ # @param value [Boolean, nil] The desired state. If `nil`, returns the current value.
158
+ # @return [Boolean] Whether :rescue events are being captured.
159
+ def capture_rescue(value = nil)
160
+ if value.nil?
161
+ @capture_rescue = @capture_rescue.nil? ? false : @capture_rescue
162
+ else
163
+ @capture_rescue = value
164
+ end
165
+ @capture_rescue
166
+ end
167
+
140
168
  # Retrieves the current skip list, initializing it with default values if not already set.
141
169
  #
142
170
  # @return [Set<Symbol>] The current skip list.
@@ -166,21 +194,20 @@ class EnhancedErrors
166
194
  # @param options [Hash] Additional configuration options.
167
195
  # @yield [void] A block for additional configuration.
168
196
  # @return [void]
169
- def enhance!(enabled: true, debug: false, capture_events: default_capture_events, **options, &block)
170
- capture_events = Array(capture_events)
197
+ def enhance!(enabled: true, debug: false, capture_events: nil, **options, &block)
171
198
  @output_format = nil
172
199
  @eligible_for_capture = nil
173
200
  @original_global_variables = nil
174
201
  if enabled == false
175
202
  @original_global_variables = nil
176
203
  @enabled = false
177
- @trace.disable if @trace
204
+ # Disable TracePoints in all threads
205
+ @traces.each_value { |trace| trace.disable } if @traces
178
206
  else
179
207
  @enabled = true
180
208
  @debug = debug
181
209
  @original_global_variables = global_variables
182
210
 
183
- validate_and_set_capture_events(capture_events)
184
211
  options.each do |key, value|
185
212
  setter_method = "#{key}="
186
213
  if respond_to?(setter_method)
@@ -195,7 +222,21 @@ class EnhancedErrors
195
222
  @config_block = block_given? ? block : nil
196
223
  instance_eval(&@config_block) if @config_block
197
224
 
198
- start_tracing
225
+ validate_and_set_capture_events(capture_events)
226
+
227
+ # Initialize @traces hash to keep track of TracePoints per thread
228
+ @traces ||= {}
229
+ # Set up TracePoint in the main thread
230
+ start_tracing(Thread.current)
231
+
232
+ # Set up TracePoint in all existing threads
233
+ Thread.list.each do |thread|
234
+ next if thread == Thread.current
235
+ start_tracing(thread)
236
+ end
237
+
238
+ # Hook into Thread creation to set up TracePoint in new threads
239
+ override_thread_new
199
240
  end
200
241
  end
201
242
 
@@ -259,7 +300,13 @@ class EnhancedErrors
259
300
  def format(captured_bindings = [], output_format = get_default_format_for_environment)
260
301
  result = binding_infos_array_to_string(captured_bindings, output_format)
261
302
  if @on_format_hook
262
- result = @on_format_hook.call(result)
303
+ begin
304
+ result = @on_format_hook.call(result)
305
+ rescue => e
306
+ # Since the on_format_hook failed, do not display the data
307
+ result = ''
308
+ # Optionally, log the error safely if logging is guaranteed not to raise exceptions
309
+ end
263
310
  else
264
311
  result = default_on_format(result)
265
312
  end
@@ -272,20 +319,10 @@ class EnhancedErrors
272
319
  # @param format [Symbol] The format to use (:json, :plaintext, :terminal).
273
320
  # @return [String] The formatted string representation of the binding information.
274
321
  def binding_infos_array_to_string(captured_bindings, format = :terminal)
275
- case format
276
- when :json
277
- Colors.enabled = false
278
- JSON.pretty_generate(captured_bindings)
279
- when :plaintext
280
- Colors.enabled = false
281
- captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
282
- when :terminal
283
- Colors.enabled = true
284
- captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
285
- else
286
- Colors.enabled = false
287
- captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
288
- end
322
+ Colors.enabled = format == :terminal
323
+ formatted_bindings = captured_bindings.map { |binding_info| binding_info_string(binding_info) }
324
+
325
+ format == :json ? JSON.pretty_generate(captured_bindings) : formatted_bindings.join("\n")
289
326
  end
290
327
 
291
328
  # Determines the default output format based on the current environment.
@@ -345,7 +382,7 @@ class EnhancedErrors
345
382
  # @return [Hash, nil] The validated binding information or `nil` if invalid.
346
383
  def validate_binding_format(binding_info)
347
384
  unless binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
348
- puts "Invalid binding_info format."
385
+ # Log or handle the invalid format as needed
349
386
  return nil
350
387
  end
351
388
  binding_info
@@ -356,10 +393,12 @@ class EnhancedErrors
356
393
  # @param binding_info [Hash] The binding information to format.
357
394
  # @return [String] The formatted string.
358
395
  def binding_info_string(binding_info)
359
- capture_event = binding_info[:capture_event].to_s.capitalize
360
- result = "#{Colors.red(capture_event)}: #{Colors.blue(binding_info[:source])}"
396
+ capture_event = safe_to_s(binding_info[:capture_event]).capitalize
397
+ source = safe_to_s(binding_info[:source])
398
+ result = "#{Colors.red(capture_event)}: #{Colors.blue(source)}"
361
399
 
362
- result += method_and_args_desc(binding_info[:method_and_args])
400
+ method_desc = method_and_args_desc(binding_info[:method_and_args])
401
+ result += method_desc
363
402
 
364
403
  variables = binding_info[:variables] || {}
365
404
 
@@ -386,22 +425,21 @@ class EnhancedErrors
386
425
  end
387
426
  result + "\n"
388
427
  rescue => e
389
- # we swallow and don't re-raise to avoid any recursion problems. We don't want to
390
- # mess up the original exception handling.
391
- puts "EnhancedErrors error in binding_info_string: #{e.message} #{e.backtrace}"
428
+ # Avoid raising exceptions during formatting
392
429
  return ''
393
430
  end
394
431
 
395
432
  private
396
433
 
397
- # Starts the TracePoint for capturing exceptions based on configured events.
434
+ # Starts the TracePoint for capturing exceptions based on configured events in a specific thread.
398
435
  #
436
+ # @param thread [Thread] The thread to start tracing in.
399
437
  # @return [void]
400
- def start_tracing
401
- return if @trace && @trace.enabled?
402
- events = @capture_events ? @capture_events.to_a : [:raise]
403
- @trace = TracePoint.new(*events) do |tp|
404
- next if Thread.current[:enhanced_errors_processing] || ignored_exception?(tp.raised_exception)
438
+ def start_tracing(thread)
439
+ return if @traces[thread]&.enabled?
440
+ events = @capture_events ? @capture_events.to_a : default_capture_events
441
+ trace = TracePoint.new(*events) do |tp|
442
+ next if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
405
443
  Thread.current[:enhanced_errors_processing] = true
406
444
  exception = tp.raised_exception
407
445
  capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
@@ -411,7 +449,6 @@ class EnhancedErrors
411
449
  next
412
450
  end
413
451
 
414
- exception = tp.raised_exception
415
452
  binding_context = tp.binding
416
453
 
417
454
  unless exception.instance_variable_defined?(:@binding_infos)
@@ -426,13 +463,13 @@ class EnhancedErrors
426
463
  }
427
464
 
428
465
  locals = binding_context.local_variables.map { |var|
429
- [var, binding_context.local_variable_get(var)]
466
+ [var, safe_local_variable_get(binding_context, var)]
430
467
  }.to_h
431
468
 
432
469
  instance_vars = binding_context.receiver.instance_variables
433
470
 
434
471
  instances = instance_vars.map { |var|
435
- [var, (binding_context.receiver.instance_variable_get(var) rescue "#<Error getting instance variable: #{$!.message}>")]
472
+ [var, safe_instance_variable_get(binding_context.receiver, var)]
436
473
  }.to_h
437
474
 
438
475
  # Extract 'let' variables from :@__memoized (RSpec specific)
@@ -453,8 +490,8 @@ class EnhancedErrors
453
490
  }.to_h
454
491
  end
455
492
 
456
- capture_event = tp.event.to_s # 'raise' or 'rescue'
457
- location = "#{tp.path}:#{tp.lineno}"
493
+ capture_event = safe_to_s(tp.event) # 'raise' or 'rescue'
494
+ location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
458
495
 
459
496
  binding_info = {
460
497
  source: location,
@@ -468,48 +505,86 @@ class EnhancedErrors
468
505
  lets: lets,
469
506
  globals: globals
470
507
  },
471
- exception: exception.class.name,
472
- capture_event: capture_event.to_s
508
+ exception: safe_to_s(exception.class.name),
509
+ capture_event: capture_event
473
510
  }
474
511
 
512
+ binding_info = default_on_capture(binding_info) # Apply default processing
513
+
475
514
  if on_capture_hook
476
- binding_info = on_capture_hook.call(binding_info)
477
- else
478
- binding_info = default_on_capture(binding_info)
515
+ begin
516
+ Thread.current[:on_capture] = true
517
+ binding_info = on_capture_hook.call(binding_info)
518
+ rescue => e
519
+ # Since the on_capture_hook failed, do not capture this binding_info
520
+ binding_info = nil
521
+ # Optionally, log the error safely if logging is guaranteed not to raise exceptions
522
+ ensure
523
+ Thread.current[:on_capture] = false
524
+ end
479
525
  end
480
526
 
481
- binding_info = validate_binding_format(binding_info)
482
-
527
+ # Proceed only if binding_info is valid
483
528
  if binding_info
484
- exception.instance_variable_get(:@binding_infos) << binding_info
485
- else
486
- puts "Invalid binding_info returned from on_capture, skipping."
529
+ binding_info = validate_binding_format(binding_info)
530
+ if binding_info
531
+ exception.instance_variable_get(:@binding_infos) << binding_info
532
+ end
487
533
  end
488
534
  rescue => e
489
- puts "Error in TracePoint block: #{e.message}"
535
+ # Avoid any code here that could raise exceptions
490
536
  ensure
491
537
  Thread.current[:enhanced_errors_processing] = false
492
538
  end
493
539
 
494
- @trace.enable
540
+ @traces[thread] = trace
541
+ trace.enable
495
542
  end
496
543
 
497
- def ignored_exception?(exception)
498
- IGNORED_EXCEPTIONS.each do |klass|
499
- return true if exception.is_a?(klass)
544
+ # Overrides Thread.new and Thread.start to ensure TracePoint is enabled in new threads.
545
+ #
546
+ # @return [void]
547
+ def override_thread_new
548
+ return if @thread_overridden
549
+ @thread_overridden = true
550
+
551
+ class << Thread
552
+ alias_method :original_new, :new
553
+
554
+ def new(*args, &block)
555
+ original_new(*args) do |*block_args|
556
+ EnhancedErrors.send(:start_tracing, Thread.current)
557
+ block.call(*block_args)
558
+ end
559
+ end
560
+
561
+ alias_method :original_start, :start
562
+
563
+ def start(*args, &block)
564
+ original_start(*args) do |*block_args|
565
+ EnhancedErrors.send(:start_tracing, Thread.current)
566
+ block.call(*block_args)
567
+ end
568
+ end
500
569
  end
501
- false
502
570
  end
503
571
 
572
+ # Checks if the exception is in the ignored exceptions list.
573
+ #
574
+ # @param exception [Exception] The exception to check.
575
+ # @return [Boolean] `true` if the exception should be ignored, otherwise `false`.
576
+ def ignored_exception?(exception)
577
+ IGNORED_EXCEPTIONS.any? { |klass| exception.is_a?(klass) }
578
+ end
504
579
 
505
580
  # Retrieves the current test name from RSpec, if available.
506
581
  #
507
582
  # @return [String, nil] The current test name or `nil` if not in a test context.
508
583
  def test_name
509
584
  if defined?(RSpec)
510
- return RSpec&.current_example&.full_description
585
+ RSpec&.current_example&.full_description
511
586
  end
512
- rescue => e
587
+ rescue
513
588
  nil
514
589
  end
515
590
 
@@ -517,32 +592,47 @@ class EnhancedErrors
517
592
  #
518
593
  # @return [Set<Symbol>] The default set of capture types
519
594
  def default_capture_events
520
- default_events = [:raise]
521
- default_events << :rescue if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
522
- Set.new(default_events)
595
+ events = [:raise]
596
+ if capture_rescue && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
597
+ events << :rescue
598
+ end
599
+ Set.new(events)
523
600
  end
524
601
 
602
+ # Validates and sets the capture events for TracePoint.
603
+ #
604
+ # @param capture_events [Array<Symbol>, nil] The events to capture.
605
+ # @return [void]
525
606
  def validate_and_set_capture_events(capture_events)
526
- if capture_events.nil? || !valid_capture_events?(capture_events)
607
+ if capture_events.nil?
608
+ @capture_events = default_capture_events
609
+ return
610
+ end
611
+
612
+ unless valid_capture_events?(capture_events)
527
613
  puts "EnhancedErrors: Invalid capture_events provided. Falling back to defaults."
528
- capture_events = default_capture_events
614
+ @capture_events = default_capture_events
615
+ return
529
616
  end
530
617
 
531
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0') && capture_events.include?(:rescue)
618
+ if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
532
619
  puts "EnhancedErrors: Warning: :rescue capture_event is not supported in Ruby versions below 3.3.0 and will be ignored."
533
- capture_events.delete(:rescue)
620
+ capture_events = capture_events - [:rescue]
534
621
  end
535
622
 
536
623
  if capture_events.empty?
537
624
  puts "No valid capture_events provided to EnhancedErrors.enhance! Falling back to defaults."
538
- capture_events = default_capture_events
625
+ @capture_events = default_capture_events
626
+ return
539
627
  end
540
628
 
541
- @capture_events = capture_events.to_a
629
+ @capture_events = capture_events.to_set
542
630
  end
543
631
 
544
-
545
- # Validate capture_events: must be an Array or Set containing only :raise and/or :rescue.
632
+ # Validates the capture events.
633
+ #
634
+ # @param capture_events [Array<Symbol>] The events to validate.
635
+ # @return [Boolean] `true` if valid, otherwise `false`.
546
636
  def valid_capture_events?(capture_events)
547
637
  return false unless capture_events.is_a?(Array) || capture_events.is_a?(Set)
548
638
  valid_types = [:raise, :rescue].to_set
@@ -564,8 +654,8 @@ class EnhancedErrors
564
654
  locals = bind.local_variables
565
655
 
566
656
  parameters.map do |(type, name)|
567
- value = locals.include?(name) ? bind.local_variable_get(name) : nil
568
- "#{name}=#{value.inspect}"
657
+ value = locals.include?(name) ? safe_local_variable_get(bind, name) : nil
658
+ "#{name}=#{safe_inspect(value)}"
569
659
  rescue => e
570
660
  "#{name}=#<Error getting argument: #{e.message}>"
571
661
  end.join(", ")
@@ -581,9 +671,9 @@ class EnhancedErrors
581
671
  # @return [String] The formatted object name.
582
672
  def determine_object_name(tp, method_name)
583
673
  if tp.self.is_a?(Class) && tp.self.singleton_class == tp.defined_class
584
- "#{tp.self}.#{method_name}"
674
+ "#{safe_to_s(tp.self)}.#{method_name}"
585
675
  else
586
- "#{tp.self.class.name}##{method_name}"
676
+ "#{safe_to_s(tp.self.class.name)}##{method_name}"
587
677
  end
588
678
  rescue => e
589
679
  "#<Error inspecting value: #{e.message}>"
@@ -597,7 +687,7 @@ class EnhancedErrors
597
687
  begin
598
688
  var.is_a?(Symbol) ? eval("#{var}") : nil
599
689
  rescue => e
600
- "#<Error getting value for #{var}>" rescue '<value error>'
690
+ "#<Error getting value for #{var}>"
601
691
  end
602
692
  end
603
693
 
@@ -606,11 +696,14 @@ class EnhancedErrors
606
696
  # @param method_info [Hash] Information about the method and its arguments.
607
697
  # @return [String] The formatted description.
608
698
  def method_and_args_desc(method_info)
609
- return '' unless method_info[:object_name] != '' || method_info[:args]&.length.to_i > 0
610
- arg_str = method_info[:args]
611
- arg_str = "(#{arg_str})" if arg_str != ""
612
- str = method_info[:object_name] + arg_str
699
+ object_name = safe_to_s(method_info[:object_name])
700
+ args = safe_to_s(method_info[:args])
701
+ return '' if object_name.empty? && args.empty?
702
+ arg_str = args.empty? ? '' : "(#{args})"
703
+ str = object_name + arg_str
613
704
  "\n#{Colors.green('Method: ')}#{Colors.blue(str)}\n"
705
+ rescue => e
706
+ ''
614
707
  end
615
708
 
616
709
  # Generates a formatted description for a set of variables.
@@ -621,26 +714,23 @@ class EnhancedErrors
621
714
  vars_hash.map do |name, value|
622
715
  " #{Colors.purple(name)}: #{format_variable(value)}\n"
623
716
  end.join
717
+ rescue => e
718
+ ''
624
719
  end
625
720
 
626
721
  # Formats a variable for display, using `awesome_print` if available and enabled.
627
722
  #
628
723
  # @param variable [Object] The variable to format.
629
724
  # @return [String] The formatted variable.
630
-
631
725
  def format_variable(variable)
632
726
  if awesome_print_available? && Colors.enabled?
633
727
  variable.ai
634
728
  else
635
- variable.inspect
729
+ safe_inspect(variable)
636
730
  end
637
731
  rescue => e
638
- var_str = begin
639
- variable.to_s.truncate(30)
640
- rescue
641
- "[Unprintable variable]"
642
- end
643
- return "#{var_str}: [Inspection Error]"
732
+ var_str = safe_to_s(variable)
733
+ "#{var_str}: [Inspection Error]"
644
734
  end
645
735
 
646
736
  # Checks if the `AwesomePrint` gem is available.
@@ -651,6 +741,53 @@ class EnhancedErrors
651
741
  @awesome_print_available = defined?(AwesomePrint)
652
742
  end
653
743
 
744
+ # Safely calls `inspect` on a variable.
745
+ #
746
+ # @param variable [Object] The variable to inspect.
747
+ # @return [String] The inspected variable or a safe fallback.
748
+ def safe_inspect(variable)
749
+ variable.inspect
750
+ rescue => e
751
+ safe_to_s(variable)
752
+ end
753
+
754
+ # Safely converts a variable to a string, handling exceptions.
755
+ #
756
+ # @param variable [Object] The variable to convert.
757
+ # @return [String] The string representation or a safe fallback.
758
+ def safe_to_s(variable)
759
+ str = variable.to_s
760
+ if str.length > 120
761
+ str[0...120] + '...'
762
+ else
763
+ str
764
+ end
765
+ rescue
766
+ "[Unprintable variable]"
767
+ end
768
+
769
+ # Safely retrieves a local variable from a binding.
770
+ #
771
+ # @param binding_context [Binding] The binding context.
772
+ # @param var_name [Symbol] The name of the local variable.
773
+ # @return [Object] The value of the local variable or a safe fallback.
774
+ def safe_local_variable_get(binding_context, var_name)
775
+ binding_context.local_variable_get(var_name)
776
+ rescue
777
+ "[Error accessing local variable #{var_name}]"
778
+ end
779
+
780
+ # Safely retrieves an instance variable from an object.
781
+ #
782
+ # @param obj [Object] The object.
783
+ # @param var_name [Symbol] The name of the instance variable.
784
+ # @return [Object] The value of the instance variable or a safe fallback.
785
+ def safe_instance_variable_get(obj, var_name)
786
+ obj.instance_variable_get(var_name)
787
+ rescue
788
+ "[Error accessing instance variable #{var_name}]"
789
+ end
790
+
654
791
  # Default implementation for the on_format hook.
655
792
  #
656
793
  # @param string [String] The formatted exception message.
@@ -1,57 +1,59 @@
1
+ # error_enhancements.rb
2
+
1
3
  module ErrorEnhancements
2
4
  def message
3
- original_message = super()
4
- if original_message.include?(variables_message)
5
+ original_message = begin
6
+ super()
7
+ rescue
8
+ ''
9
+ end
10
+ vars_message = variables_message rescue ""
11
+ if original_message.include?(vars_message)
5
12
  original_message
6
13
  else
7
- "#{original_message}\n#{variables_message}"
14
+ "#{original_message}\n#{vars_message}"
8
15
  end
9
16
  rescue => e
10
- original_message
17
+ original_message || ''
11
18
  end
12
19
 
13
20
  def variables_message
14
21
  @variables_message ||= begin
15
- bindings_of_interest = []
16
- if defined?(@binding_infos) && @binding_infos && !@binding_infos.empty?
22
+ if @binding_infos&.any?
17
23
  bindings_of_interest = select_binding_infos(@binding_infos)
24
+ EnhancedErrors.format(bindings_of_interest)
25
+ else
26
+ ''
18
27
  end
19
- EnhancedErrors.format(bindings_of_interest)
20
- rescue => e
21
- puts "Error in variables_message: #{e.message}"
22
- ""
28
+ rescue
29
+ ''
23
30
  end
24
31
  end
25
32
 
26
33
  private
27
34
 
28
- def select_binding_infos(binding_infos)
29
- # Preference:
30
- # Grab the first raise binding that isn't a library (gem) binding.
31
- # If there are only library bindings, grab the first one.
32
- # Grab the last rescue binding if we have one
35
+ def select_binding_infos(binding_infos)
36
+ # Preference:
37
+ # 1. First 'raise' binding that isn't from a library (gem).
38
+ # 2. If none, the first binding.
39
+ # 3. The last 'rescue' binding if available.
33
40
 
34
- bindings_of_interest = []
41
+ bindings_of_interest = []
35
42
 
36
- binding_infos.each do |info|
37
- if info[:capture_event] == 'raise' && !info[:library]
38
- bindings_of_interest << info
39
- break
43
+ first_app_raise = binding_infos.find do |info|
44
+ info[:capture_event] == 'raise' && !info[:library]
40
45
  end
41
- end
46
+ bindings_of_interest << first_app_raise if first_app_raise
42
47
 
43
- if bindings_of_interest.empty?
44
- bindings_of_interest << binding_infos.first if binding_infos.first
45
- end
48
+ if bindings_of_interest.empty? && binding_infos.first
49
+ bindings_of_interest << binding_infos.first
50
+ end
46
51
 
47
- # find the last rescue binding if there is one
48
- binding_infos.reverse.each do |info|
49
- if info[:capture_event] == 'rescue'
50
- bindings_of_interest << info
51
- break
52
+ last_rescue = binding_infos.reverse.find do |info|
53
+ info[:capture_event] == 'rescue'
52
54
  end
53
- end
54
- bindings_of_interest
55
- end
55
+ bindings_of_interest << last_rescue if last_rescue
56
56
 
57
+ bindings_of_interest
58
+ end
57
59
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: enhanced_errors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Beland
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-04 00:00:00.000000000 Z
11
+ date: 2024-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_print