Skip to content

Commit a80335b

Browse files
authored
Merge pull request #8908 from headius/inspect_hash_keys
Update Hash#inspect logic for multibyte keys
2 parents 25dc482 + 2ca2938 commit a80335b

File tree

4 files changed

+80
-16
lines changed

4 files changed

+80
-16
lines changed

core/src/main/java/org/jruby/RubyHash.java

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -950,20 +950,66 @@ protected RubyString inspectHash(final ThreadContext context) {
950950

951951
private static final VisitorWithState<RubyString> InspectVisitor = new VisitorWithState<RubyString>() {
952952
@Override
953+
// MRI: inspect_i for inspect_hash in hash.c
953954
public void visit(ThreadContext context, RubyHash self, IRubyObject key, IRubyObject value, int index, RubyString str) {
954-
RubyString keyStr = inspect(context, key);
955-
RubyString valStr = inspect(context, value);
955+
RubyString keyStr;
956956

957-
final ByteList bytes = str.getByteList();
958-
bytes.ensure(2 + keyStr.size() + 2 + valStr.size());
957+
boolean isSymbol = key instanceof RubySymbol;
958+
boolean quote = false;
959+
if (key instanceof RubySymbol symbol) {
960+
keyStr = symbol.fstring();
961+
quote = symbolKeyNeedsQuote(context, symbol);
962+
} else {
963+
keyStr = inspect(context, key);
964+
}
965+
966+
if (str.size() > 1) {
967+
str.catString(", ");
968+
} else {
969+
str.setEncoding(keyStr.getEncoding());
970+
}
959971

960-
if (index > 0) bytes.append((byte) ',').append((byte) ' ');
972+
if (quote) {
973+
str.append(keyStr.inspect(context));
974+
} else {
975+
str.append(keyStr);
976+
}
961977

962-
boolean keyIsSymbol = key instanceof RubySymbol;
978+
str.catString(isSymbol ? ": " : " => ");
963979

964-
str.catWithCodeRange(keyIsSymbol ? (RubyString) keyStr.substr(context, 1, keyStr.length() - 1) : keyStr);
965-
self.appendAssociation(keyIsSymbol, bytes);
966-
str.catWithCodeRange(valStr);
980+
RubyString valStr = inspect(context, value);
981+
str.append(valStr);
982+
}
983+
984+
// MRI: symbol_key_needs_quote
985+
boolean symbolKeyNeedsQuote(ThreadContext context, RubySymbol sym) {
986+
RubyString str = sym.fstring();
987+
int len = str.size();
988+
if (len == 0 || !sym.isSimpleName(context)) return true;
989+
ByteList bytes = str.getByteList();
990+
int first = bytes.get(0);
991+
if (first == '@' || first == '$' || first == '!') return true;
992+
if (!RubyString.atCharBoundary(bytes.unsafeBytes(), bytes.begin(), bytes.begin() + bytes.getRealSize() - 1, bytes.begin() + bytes.realSize(), bytes.getEncoding())) return false;
993+
switch (bytes.get(len - 1)) {
994+
case '+':
995+
case '-':
996+
case '*':
997+
case '/':
998+
case '`':
999+
case '%':
1000+
case '^':
1001+
case '&':
1002+
case '|':
1003+
case ']':
1004+
case '<':
1005+
case '=':
1006+
case '>':
1007+
case '~':
1008+
case '@':
1009+
return true;
1010+
default:
1011+
return false;
1012+
}
9671013
}
9681014
};
9691015

core/src/main/java/org/jruby/RubyString.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4178,7 +4178,7 @@ private void ensureBytePosition(ThreadContext context, int pos) {
41784178
}
41794179

41804180
// MRI: at_char_boundary
4181-
private static boolean atCharBoundary(byte[] bytes, int s, int p, int e, Encoding enc) {
4181+
public static boolean atCharBoundary(byte[] bytes, int s, int p, int e, Encoding enc) {
41824182
// our version checks if p == bytes.length, where CRuby would have a \0 character and not reposition
41834183
return p == bytes.length || enc.leftAdjustCharHead(bytes, s, p, e) == p;
41844184
}

core/src/main/java/org/jruby/RubySymbol.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -485,14 +485,9 @@ final RubyString inspect(final Ruby runtime) {
485485

486486
@JRubyMethod(name = "inspect")
487487
public IRubyObject inspect(ThreadContext context) {
488-
// TODO: 1.9 rb_enc_symname_p
489-
Encoding resenc = context.runtime.getDefaultInternalEncoding();
490-
if (resenc == null) resenc = context.runtime.getDefaultExternalEncoding();
491-
492488
RubyString str = newString(context, getBytes());
493489

494-
if (!(isPrintable(context) && (resenc.equals(getBytes().getEncoding()) || str.isAsciiOnly()) &&
495-
isSymbolName(symbol))) {
490+
if (!isSimpleName(context)) {
496491
str = (RubyString) str.inspect(context);
497492
}
498493

@@ -504,6 +499,24 @@ public IRubyObject inspect(ThreadContext context) {
504499
return newString(context, result);
505500
}
506501

502+
// MRI: rb_str_symname_p
503+
public boolean isSimpleName(ThreadContext context) {
504+
int len;
505+
Ruby runtime = context.runtime;
506+
Encoding resenc = runtime.getDefaultInternalEncoding();
507+
if (resenc == null) resenc = runtime.getDefaultExternalEncoding();
508+
509+
RubyString str = fstring;
510+
ByteList bytes = fstring.getByteList();
511+
Encoding enc = bytes.getEncoding();
512+
513+
if ((resenc != enc && !str.isAsciiOnly()) /*|| len != (long)strlen(ptr)*/ ||
514+
!isSymbolName(symbol) || !isPrintable(context)) {
515+
return false;
516+
}
517+
return true;
518+
}
519+
507520
@Override
508521
@JRubyMethod(name = "to_s")
509522
public IRubyObject to_s(ThreadContext context) {
@@ -874,6 +887,7 @@ private boolean isPrintable(ThreadContext context) {
874887
return true;
875888
}
876889

890+
// MRI: rb_enc_symname_p, roughly
877891
private static boolean isSymbolName(final String str) {
878892
if (str == null || str.isEmpty()) return false;
879893

spec/ruby/core/hash/shared/to_s.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
{ true => false }.to_s.should == expected
8686
end
8787

88+
it "leaves printable multi-byte symbolic keys unescaped" do
89+
{"\u3042": 1}.inspect.should == "{あ: 1}"
90+
end
91+
8892
ruby_version_is "3.4" do
8993
it "adds quotes to symbol keys that are not valid symbol literals" do
9094
{ "needs-quotes": 1 }.send(@method).should == '{"needs-quotes": 1}'

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy