require 'stringio' module Kernel # Prints object with bonus info such as file name, line number and source # expression. Optionally prints out header and footer. # Lookup PutsDebuggerer attributes for more details about configuration options. # # Simply invoke global `pd` method anywhere you'd like to see line number and source code with output. # If the argument is a pure string, the print out is simplified by not showing duplicate source. # # Quickly locate printed lines using Find feature (e.g. CTRL+F) by looking for: # * \[PD\] # * file:line_number # * ruby expression. # # This gives you the added benefit of easily removing your pd statements later on from the code. # # Happy puts_debuggerering! # # Example Code: # # # /Users/User/finance_calculator_app/pd_test.rb # line 1 # bug = 'beattle' # line 2 # pd "Show me the source of the bug: #{bug}" # line 3 # pd 'What line number am I?' # line 4 # # Example Printout: # # [PD] /Users/User/finance_calculator_app/pd_test.rb:3 # > pd "Show me the source of the bug: #{bug}" # => "Show me the source of the bug: beattle" # [PD] /Users/User/finance_calculator_app/pd_test.rb:4 "What line number am I?" def pd(*objects) options = PutsDebuggerer.determine_options(objects) || {} object = PutsDebuggerer.determine_object(objects) run_at = PutsDebuggerer.determine_run_at(options) printer = PutsDebuggerer.determine_printer(options) pd_inspect = options.delete(:pd_inspect) logger_formatter_decorated = PutsDebuggerer.printer.is_a?(Logger) && PutsDebuggerer.printer.formatter != PutsDebuggerer.logger_original_formatter logging_layouts_decorated = PutsDebuggerer.printer.is_a?(Logging::Logger) && PutsDebuggerer.printer.appenders.map(&:layout) != (PutsDebuggerer.logging_original_layouts.values) string = nil if PutsDebuggerer::RunDeterminer.run_pd?(object, run_at) __with_pd_options__(options) do |print_engine_options| run_number = PutsDebuggerer::RunDeterminer.run_number(object, run_at) formatter_pd_data = __build_pd_data__(object, print_engine_options: print_engine_options, source_line_count: PutsDebuggerer.source_line_count, run_number: run_number, pd_inspect: pd_inspect, logger_formatter_decorated: logger_formatter_decorated, logging_layouts_decorated: logging_layouts_decorated) stdout = $stdout $stdout = sio = StringIO.new PutsDebuggerer.formatter.call(formatter_pd_data) $stdout = stdout string = sio.string if RUBY_ENGINE == 'opal' && object.is_a?(Exception) $stderr.puts(string) else if PutsDebuggerer.printer.is_a?(Proc) PutsDebuggerer.printer.call(string) elsif PutsDebuggerer.printer.is_a?(Logger) logger_formatter = PutsDebuggerer.printer.formatter begin PutsDebuggerer.printer.formatter = PutsDebuggerer.logger_original_formatter PutsDebuggerer.printer.debug(string) ensure PutsDebuggerer.printer.formatter = logger_formatter end elsif PutsDebuggerer.printer.is_a?(Logging::Logger) logging_layouts = PutsDebuggerer.printer.appenders.reduce({}) do |hash, appender| hash.merge(appender => appender.layout) end begin PutsDebuggerer.logging_original_layouts.each do |appender, original_layout| appender.layout = original_layout end PutsDebuggerer.printer.debug(string) ensure PutsDebuggerer.logging_original_layouts.each do |appender, original_layout| appender.layout = logging_layouts[appender] end end elsif PutsDebuggerer.printer != false send(PutsDebuggerer.send(:printer), string) end end end end printer ? object : string end # Implement caller backtrace method in Opal since it returns an empty array in Opal v1 if RUBY_ENGINE == 'opal' def caller(*args) dup_args = args.dup start = args.shift if args.first.is_a?(Integer) length = args.shift if args.first.is_a?(Integer) range = args.shift if args.first.is_a?(Range) if range start = range.begin length = range.end - start end begin raise 'error' rescue => e the_backtrace = e.backtrace start ||= 0 start = 2 + start length ||= the_backtrace.size - start the_backtrace[start, length] end end end def pd_inspect pd self, printer: false, pd_inspect: true end alias pdi pd_inspect # Provides caller line number starting 1 level above caller of # this method. # # Example: # # # lib/example.rb # line 1 # puts "Print out __caller_line_number__" # line 2 # puts __caller_line_number__ # line 3 # # prints out `3` def __caller_line_number__(caller_depth=0) return if RUBY_ENGINE == 'opal' caller[caller_depth] && caller[caller_depth][PutsDebuggerer::STACK_TRACE_CALL_LINE_NUMBER_REGEX, 1].to_i end # Provides caller file starting 1 level above caller of # this method. # # Example: # # # File Name: lib/example.rb # puts __caller_file__ # # prints out `lib/example.rb` def __caller_file__(caller_depth=0) regex = RUBY_ENGINE == 'opal' ? PutsDebuggerer::STACK_TRACE_CALL_SOURCE_FILE_REGEX_OPAL : PutsDebuggerer::STACK_TRACE_CALL_SOURCE_FILE_REGEX caller[caller_depth] && caller[caller_depth][regex, 1] end # Provides caller method starting 1 level above caller of # this method. def __caller_method__(caller_depth=0) regex = PutsDebuggerer::STACK_TRACE_CALL_METHOD_REGEX caller[caller_depth] && caller[caller_depth][regex, 1] end # Provides caller source line starting 1 level above caller of # this method. # # Example: # # puts __caller_source_line__ # # prints out `puts __caller_source_line__` def __caller_source_line__(caller_depth=0, source_line_count=nil, source_file=nil, source_line_number=nil) source_line_number ||= __caller_line_number__(caller_depth+1) source_file ||= __caller_file__(caller_depth+1) source_line = '' if defined?(Pry) && source_file.include?('(pry)') @pry_instance ||= Pry.new source_line = Pry::Command::Hist.new(pry_instance: @pry_instance).call.instance_variable_get(:@buffer).split("\n")[source_line_number - 1] # TODO handle multi-lines in source_line_count elsif defined?(IRB) && TOPLEVEL_BINDING.receiver.respond_to?(:conf) source_line = TOPLEVEL_BINDING.receiver.conf.io.line(source_line_number) # TODO handle multi-lines in source_line_count else source_line = PutsDebuggerer::SourceFile.new(source_file).source(source_line_count, source_line_number) end source_line end private def __with_pd_options__(options=nil) options ||= {} permanent_options = PutsDebuggerer.options PutsDebuggerer.options = options.select {|option, _| PutsDebuggerer.options.keys.include?(option)} print_engine_options = options.delete_if {|option, _| PutsDebuggerer.options.keys.include?(option)} yield print_engine_options PutsDebuggerer.options = permanent_options end def __build_pd_data__(object, print_engine_options:nil, source_line_count:nil, run_number:nil, pd_inspect:false, logger_formatter_decorated:false, logging_layouts_decorated:false) depth = RUBY_ENGINE == 'opal' ? PutsDebuggerer::CALLER_DEPTH_ZERO_OPAL : PutsDebuggerer::CALLER_DEPTH_ZERO if pd_inspect depth += 1 depth += 4 if logger_formatter_decorated depth += 8 if logging_layouts_decorated end pd_data = { announcer: PutsDebuggerer.announcer, file: __caller_file__(depth)&.sub(PutsDebuggerer.app_path.to_s, ''), class: self.is_a?(Module) ? self : self.class, method: __caller_method__(depth)&.sub(PutsDebuggerer.app_path.to_s, ''), line_number: __caller_line_number__(depth), pd_expression: __caller_pd_expression__(depth, source_line_count), run_number: run_number, object: object, object_printer: PutsDebuggerer::OBJECT_PRINTER_DEFAULT.call(object, print_engine_options, source_line_count, run_number) } pd_data[:caller] = __caller_caller__(depth) ['header', 'wrapper', 'footer'].each do |boundary_option| pd_data[boundary_option.to_sym] = PutsDebuggerer.send(boundary_option) if PutsDebuggerer.send("#{boundary_option}?") end pd_data end # Returns the caller stack trace of the caller of pd def __caller_caller__(depth) return unless PutsDebuggerer.caller? start_depth = depth.to_i + 1 caller_depth = PutsDebuggerer.caller == -1 ? -1 : (start_depth + PutsDebuggerer.caller) caller[start_depth..caller_depth].to_a end def __format_pd_expression__(expression, object) "\n > #{expression}\n =>" end def __caller_pd_expression__(depth=0, source_line_count=nil) # Caller Source Line Depth 2 = 1 to pd method + 1 to caller source_line = __caller_source_line__(depth+1, source_line_count) source_line = __extract_pd_expression__(source_line) source_line = source_line.gsub(/(^'|'$)/, '"') if source_line.start_with?("'") && source_line.end_with?("'") source_line = source_line.gsub(/(^\(|\)$)/, '') if source_line.start_with?("(") && source_line.end_with?(")") source_line end # Extracts pd source line expression. # # Example: # # __extract_pd_expression__("pd (x=1)") # # outputs `(x=1)` def __extract_pd_expression__(source_line) source_line.to_s.strip end end