Skip to content
64 changes: 33 additions & 31 deletions lib/pp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,31 +145,22 @@ module PPMethods
# Yields to a block
# and preserves the previous set of objects being printed.
def guard_inspect_key
if Thread.current[:__recursive_key__] == nil
Thread.current[:__recursive_key__] = {}.compare_by_identity
end

if Thread.current[:__recursive_key__][:inspect] == nil
Thread.current[:__recursive_key__][:inspect] = {}.compare_by_identity
end

save = Thread.current[:__recursive_key__][:inspect]

recursive_state = Thread.current[:__recursive_key__] ||= {}.compare_by_identity
save = recursive_state[:inspect] ||= {}.compare_by_identity
begin
Thread.current[:__recursive_key__][:inspect] = {}.compare_by_identity
recursive_state[:inspect] = {}.compare_by_identity
yield
ensure
Thread.current[:__recursive_key__][:inspect] = save
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)
Thread.current[:__recursive_key__] &&
Thread.current[:__recursive_key__][:inspect] &&
Thread.current[:__recursive_key__][:inspect].include?(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
Expand All @@ -186,7 +177,7 @@ def pop_inspect_key(id)
private def guard_inspect(object)
recursive_state = Thread.current[:__recursive_key__]

if recursive_state && recursive_state.key?(:inspect)
if recursive_state&.key?(:inspect)
begin
push_inspect_key(object)
yield
Expand Down Expand Up @@ -322,12 +313,10 @@ def pp_hash(obj)
# A pretty print for a pair of Hash
def pp_hash_pair(k, v)
if Symbol === k
sym_s = k.inspect
if sym_s[1].match?(/["$@!]/) || sym_s[-1].match?(/[%&*+\-\/<=>@\]^`|~]/)
text "#{k.to_s.inspect}:"
else
text "#{k}:"
if k.inspect.match?(%r[\A:["$@!]|[%&*+\-\/<=>@\]^`|~]\z])
k = k.to_s.inspect
end
text "#{k}:"
else
pp k
text ' '
Expand Down Expand Up @@ -399,7 +388,8 @@ def pretty_print_cycle(q)
# This method should return an array of names of instance variables as symbols or strings as:
# +[:@a, :@b]+.
def pretty_print_instance_variables
instance_variables.sort
ivars = respond_to?(:instance_variables_to_inspect) ? instance_variables_to_inspect : instance_variables
ivars.sort
end

# Is #inspect implementation using #pretty_print.
Expand Down Expand Up @@ -442,22 +432,27 @@ def pretty_print_cycle(q) # :nodoc:
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, '#<Set:', '>') {
pp.breakable
pp.group(1, '{', '}') {
pp.seplist(self) { |o|
pp.pp o
}
pp.group(1, "#{self.class.name}[", ']') {
pp.seplist(self) { |o|
pp.pp o
}
}
end

def pretty_print_cycle(pp) # :nodoc:
pp.text sprintf('#<Set: {%s}>', empty? ? '' : '...')
name = self.class.name
pp.text(empty? ? "#{name}[]" : "#{name}[...]")
end
end
end if set_pp

class << ENV # :nodoc:
def pretty_print(q) # :nodoc:
Expand Down Expand Up @@ -489,6 +484,13 @@ def pretty_print_cycle(q) # :nodoc:
end
end

verbose, $VERBOSE = $VERBOSE, nil
begin
has_data_define = defined?(Data.define)
ensure
$VERBOSE = verbose
end

class Data # :nodoc:
def pretty_print(q) # :nodoc:
class_name = PP.mcall(self, Kernel, :class).name
Expand Down Expand Up @@ -521,7 +523,7 @@ def pretty_print(q) # :nodoc:
def pretty_print_cycle(q) # :nodoc:
q.text sprintf("#<data %s:...>", PP.mcall(self, Kernel, :class).name)
end
end if defined?(Data.define)
end if has_data_define

class Range # :nodoc:
def pretty_print(q) # :nodoc:
Expand Down
7 changes: 6 additions & 1 deletion spec/ruby/core/set/pretty_print_cycle_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
describe "Set#pretty_print_cycle" do
it "passes the 'pretty print' representation of a self-referencing Set to the pretty print writer" do
pp = mock("PrettyPrint")
pp.should_receive(:text).with("#<Set: {...}>")
ruby_version_is(""..."3.5") do
pp.should_receive(:text).with("#<Set: {...}>")
end
ruby_version_is("3.5") do
pp.should_receive(:text).with("Set[...]")
end
Set[1, 2, 3].pretty_print_cycle(pp)
end
end
56 changes: 51 additions & 5 deletions test/test_pp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

require 'pp'
require 'delegate'
require 'set'
require 'test/unit'
require 'ruby2_keywords'

module PPTestModule

SetPP = Set.instance_method(:pretty_print).source_location[0].end_with?("/pp.rb")

class PPTest < Test::Unit::TestCase
def test_list0123_12
assert_equal("[0, 1, 2, 3]\n", PP.pp([0,1,2,3], ''.dup, 12))
Expand All @@ -16,6 +19,10 @@ def test_list0123_11
assert_equal("[0,\n 1,\n 2,\n 3]\n", PP.pp([0,1,2,3], ''.dup, 11))
end

def test_set
assert_equal("Set[0, 1, 2, 3]\n", PP.pp(Set[0,1,2,3], ''.dup, 16))
end if SetPP

OverriddenStruct = Struct.new("OverriddenStruct", :members, :class)
def test_struct_override_members # [ruby-core:7865]
a = OverriddenStruct.new(1,2)
Expand Down Expand Up @@ -130,6 +137,20 @@ def a.to_s() "aaa" end
assert_equal("#{a.inspect}\n", result)
end

def test_iv_hiding
a = Object.new
def a.pretty_print_instance_variables() [:@b] end
a.instance_eval { @a = "aaa"; @b = "bbb" }
assert_match(/\A#<Object:0x[\da-f]+ @b="bbb">\n\z/, PP.pp(a, ''.dup))
end

def test_iv_hiding_via_ruby
a = Object.new
def a.instance_variables_to_inspect() [:@b] end
a.instance_eval { @a = "aaa"; @b = "bbb" }
assert_match(/\A#<Object:0x[\da-f]+ @b="bbb">\n\z/, PP.pp(a, ''.dup))
end

def test_basic_object
a = BasicObject.new
assert_match(/\A#<BasicObject:0x[\da-f]+>\n\z/, PP.pp(a, ''.dup))
Expand All @@ -150,6 +171,12 @@ def test_hash
assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup))
end

def test_set
s = Set[]
s.add s
assert_equal("Set[Set[...]]\n", PP.pp(s, ''.dup))
end if SetPP

S = Struct.new("S", :a, :b)
def test_struct
a = S.new(1,2)
Expand All @@ -158,7 +185,14 @@ def test_struct
assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup)) unless RUBY_ENGINE == "truffleruby"
end

if defined?(Data.define)
verbose, $VERBOSE = $VERBOSE, nil
begin
has_data_define = defined?(Data.define)
ensure
$VERBOSE = verbose
end

if has_data_define
D = Data.define(:aaa, :bbb)
def test_data
a = D.new("aaa", "bbb")
Expand Down Expand Up @@ -223,7 +257,6 @@ def test_hash
end

def test_hash_symbol_colon_key
omit if RUBY_VERSION < "3.4."
no_quote = "{a: 1, a!: 1, a?: 1}"
unicode_quote = "{\u{3042}: 1}"
quote0 = '{"": 1}'
Expand All @@ -236,12 +269,25 @@ def test_hash_symbol_colon_key
assert_equal(quote1, PP.singleline_pp(eval(quote1), ''.dup))
assert_equal(quote2, PP.singleline_pp(eval(quote2), ''.dup))
assert_equal(quote3, PP.singleline_pp(eval(quote3), ''.dup))
end
end if RUBY_VERSION >= "3.4."

def test_hash_in_array
omit if RUBY_ENGINE == "jruby"
assert_equal("[{}]", PP.singleline_pp([->(*a){a.last.clear}.ruby2_keywords.call(a: 1)], ''.dup))
assert_equal("[{}]", PP.singleline_pp([Hash.ruby2_keywords_hash({})], ''.dup))
assert_equal("[{}]", passing_keywords {PP.singleline_pp([->(*a){a.last.clear}.ruby2_keywords.call(a: 1)], ''.dup)})
assert_equal("[{}]", passing_keywords {PP.singleline_pp([Hash.ruby2_keywords_hash({})], ''.dup)})
end

if RUBY_VERSION >= "3.0"
def passing_keywords(&_)
yield
end
else
def passing_keywords(&_)
verbose, $VERBOSE = $VERBOSE, nil
yield
ensure
$VERBOSE = verbose
end
end

def test_direct_pp
Expand Down