# frozen_string_literal: true
require 'prettyprint'
##
# A pretty-printer for Ruby objects.
#
##
# == What PP Does
#
# Standard output by #p returns this:
# ##pretty_print(pp) in the class.
# Note that require 'pp' is needed before redefining #pretty_print(pp).
#
# #pretty_print takes the +pp+ argument, which is an instance of the PP class.
# The method uses #text, #breakable, #nest, #group and #pp to print the
# object.
#
##
# == Pretty-Print JSON
#
# To pretty-print JSON refer to JSON#pretty_generate.
#
##
# == Author
# Tanaka Akira $> is assumed.
# If +width+ is omitted, the width of +out+ is assumed (see
# width_for).
#
# PP.pp returns +out+.
def PP.pp(obj, out=$>, width=width_for(out))
q = new(out, width)
q.guard_inspect_key {q.pp obj}
q.flush
#$pp = q
out << "\n"
end
# Outputs +obj+ to +out+ like PP.pp but with no indent and
# newline.
#
# PP.singleline_pp returns +out+.
def PP.singleline_pp(obj, out=$>)
q = SingleLine.new(out)
q.guard_inspect_key {q.pp obj}
q.flush
out
end
# :stopdoc:
def PP.mcall(obj, mod, meth, *args, &block)
mod.instance_method(meth).bind_call(obj, *args, &block)
end
# :startdoc:
if defined? ::Ractor
class << self
# Returns the sharing detection flag as a boolean value.
# It is false (nil) by default.
def sharing_detection
Ractor.current[:pp_sharing_detection]
end
# Sets the sharing detection flag to b.
def sharing_detection=(b)
Ractor.current[:pp_sharing_detection] = b
end
end
else
@sharing_detection = false
class << self
# Returns the sharing detection flag as a boolean value.
# It is false by default.
attr_accessor :sharing_detection
end
end
# Module that defines helper methods for pretty_print.
module PPMethods
# Yields to a block
# and preserves the previous set of objects being printed.
def guard_inspect_key
recursive_state = Thread.current[:__recursive_key__] ||= {}.compare_by_identity
save = recursive_state[:inspect] ||= {}.compare_by_identity
begin
recursive_state[:inspect] = {}.compare_by_identity
yield
ensure
recursive_state[:inspect] = save
end
end
# Check whether the object_id +id+ is in the current buffer of objects
# to be pretty printed. Used to break cycles in chains of objects to be
# pretty printed.
def check_inspect_key(id)
recursive_state = Thread.current[:__recursive_key__] or return false
recursive_state[:inspect]&.include?(id)
end
# Adds the object_id +id+ to the set of objects being pretty printed, so
# as to not repeat objects.
def push_inspect_key(id)
Thread.current[:__recursive_key__][:inspect][id] = true
end
# Removes an object from the set of objects being pretty printed.
def pop_inspect_key(id)
Thread.current[:__recursive_key__][:inspect].delete id
end
private def guard_inspect(object) # :nodoc:
recursive_state = Thread.current[:__recursive_key__]
if recursive_state&.key?(:inspect)
begin
push_inspect_key(object)
yield
ensure
pop_inspect_key(object) unless PP.sharing_detection
end
else
guard_inspect_key do
push_inspect_key(object)
yield
end
end
end
# Adds +obj+ to the pretty printing buffer
# using Object#pretty_print or Object#pretty_print_cycle.
#
# Object#pretty_print_cycle is used when +obj+ is already
# printed, a.k.a the object reference chain has a cycle.
def pp(obj)
# If obj is a Delegator then use the object being delegated to for cycle
# detection
obj = obj.__getobj__ if defined?(::Delegator) and ::Delegator === obj
if check_inspect_key(obj)
group {obj.pretty_print_cycle self}
return
end
guard_inspect(obj) do
group do
obj.pretty_print self
rescue NoMethodError
text Kernel.instance_method(:inspect).bind_call(obj)
end
end
end
# A convenience method which is same as follows:
#
# group(1, '#<' + obj.class.name, '>') { ... }
def object_group(obj, &block) # :yield:
group(1, '#<' + obj.class.name, '>', &block)
end
# A convenience method, like object_group, but also reformats the Object's
# object_id.
def object_address_group(obj, &block)
str = Kernel.instance_method(:to_s).bind_call(obj)
str.chomp!('>')
group(1, str, '>', &block)
end
# A convenience method which is same as follows:
#
# text ','
# breakable
def comma_breakable
text ','
breakable
end
# Adds a separated list.
# The list is separated by comma with breakable space, by default.
#
# #seplist iterates the +list+ using +iter_method+.
# It yields each object to the block given for #seplist.
# The procedure +separator_proc+ is called between each yields.
#
# If the iteration is zero times, +separator_proc+ is not called at all.
#
# If +separator_proc+ is nil or not given,
# +lambda { comma_breakable }+ is used.
# If +iter_method+ is not given, :each is used.
#
# For example, following 3 code fragments has similar effect.
#
# q.seplist([1,2,3]) {|v| xxx v }
#
# q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v }
#
# xxx 1
# q.comma_breakable
# xxx 2
# q.comma_breakable
# xxx 3
def seplist(list, sep=nil, iter_method=:each) # :yield: element
sep ||= lambda { comma_breakable }
first = true
kwsplat = EMPTY_KWHASH
list.__send__(iter_method) {|*v|
if first
first = false
else
sep.call
end
kwsplat ? yield(*v, **kwsplat) : yield(*v)
}
end
EMPTY_KWHASH = if RUBY_VERSION >= "3.0" # :nodoc:
{}.freeze
end
private_constant :EMPTY_KWHASH
# A present standard failsafe for pretty printing any given Object
def pp_object(obj)
object_address_group(obj) {
seplist(obj.pretty_print_instance_variables, lambda { text ',' }) {|v|
breakable
v = v.to_s if Symbol === v
text v
text '='
group(1) {
breakable ''
pp(obj.instance_eval(v))
}
}
}
end
# A pretty print for a Hash
def pp_hash(obj)
group(1, '{', '}') {
seplist(obj, nil, :each_pair) {|k, v|
group {
pp_hash_pair k, v
}
}
}
end
if RUBY_VERSION >= '3.4.'
# A pretty print for a pair of Hash
def pp_hash_pair(k, v)
if Symbol === k
if k.inspect.match?(%r[\A:["$@!]|[%&*+\-\/<=>@\]^`|~]\z])
k = k.to_s.inspect
end
text "#{k}:"
else
pp k
text ' '
text '=>'
end
group(1) {
breakable
pp v
}
end
else
def pp_hash_pair(k, v)
pp k
text '=>'
group(1) {
breakable ''
pp v
}
end
end
end
include PPMethods
class SingleLine < PrettyPrint::SingleLine # :nodoc:
include PPMethods
end
module ObjectMixin # :nodoc:
# 1. specific pretty_print
# 2. specific inspect
# 3. generic pretty_print
# A default pretty printing method for general objects.
# It calls #pretty_print_instance_variables to list instance variables.
#
# If +self+ has a customized (redefined) #inspect method,
# the result of self.inspect is used but it obviously has no
# line break hints.
#
# This module provides predefined #pretty_print methods for some of
# the most commonly used built-in classes for convenience.
def pretty_print(q)
umethod_method = Object.instance_method(:method)
begin
inspect_method = umethod_method.bind_call(self, :inspect)
rescue NameError
end
if inspect_method && inspect_method.owner != Kernel
q.text self.inspect
elsif !inspect_method && self.respond_to?(:inspect)
q.text self.inspect
else
q.pp_object(self)
end
end
# A default pretty printing method for general objects that are
# detected as part of a cycle.
def pretty_print_cycle(q)
q.object_address_group(self) {
q.breakable
q.text '...'
}
end
# Returns a sorted array of instance variable names.
#
# This method should return an array of names of instance variables as symbols or strings as:
# +[:@a, :@b]+.
def pretty_print_instance_variables
ivars = respond_to?(:instance_variables_to_inspect, true) ? instance_variables_to_inspect || instance_variables : instance_variables
ivars.sort
end
# Is #inspect implementation using #pretty_print.
# If you implement #pretty_print, it can be used as follows.
#
# alias inspect pretty_print_inspect
#
# However, doing this requires that every class that #inspect is called on
# implement #pretty_print, or a RuntimeError will be raised.
def pretty_print_inspect
if Object.instance_method(:method).bind_call(self, :pretty_print).owner == PP::ObjectMixin
raise "pretty_print is not overridden for #{self.class}"
end
PP.singleline_pp(self, ''.dup)
end
end
end
class Array # :nodoc:
def pretty_print(q) # :nodoc:
q.group(1, '[', ']') {
q.seplist(self) {|v|
q.pp v
}
}
end
def pretty_print_cycle(q) # :nodoc:
q.text(empty? ? '[]' : '[...]')
end
end
class Hash # :nodoc:
def pretty_print(q) # :nodoc:
q.pp_hash self
end
def pretty_print_cycle(q) # :nodoc:
q.text(empty? ? '{}' : '{...}')
end
end
if defined?(Set)
if set_pp = Set.instance_method(:initialize).source_location
set_pp = !set_pp.first.end_with?("/set.rb") # not defined in set.rb
else
set_pp = true # defined in C
end
end
class Set # :nodoc:
def pretty_print(pp) # :nodoc:
pp.group(1, "#{self.class.name}[", ']') {
pp.seplist(self) { |o|
pp.pp o
}
}
end
def pretty_print_cycle(pp) # :nodoc:
name = self.class.name
pp.text(empty? ? "#{name}[]" : "#{name}[...]")
end
end if set_pp
class << ENV # :nodoc:
def pretty_print(q) # :nodoc:
h = {}
ENV.keys.sort.each {|k|
h[k] = ENV[k]
}
q.pp_hash h
end
end
class Struct # :nodoc:
def pretty_print(q) # :nodoc:
q.group(1, sprintf("#