Skip to content

Commit 2d13784

Browse files
committed
Add JRuby 10 inspectless logic for NameError
Configure with -Xinspect.nameError.object=true|false. It defaults to true in JRuby 9.4.10.0 and false in JRuby 10. Fixes #8384
1 parent 353aa59 commit 2d13784

File tree

2 files changed

+111
-4
lines changed

2 files changed

+111
-4
lines changed

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

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,15 @@
3737
import org.jruby.exceptions.NameError;
3838
import org.jruby.exceptions.RaiseException;
3939
import org.jruby.runtime.Block;
40+
import org.jruby.runtime.Helpers;
4041
import org.jruby.runtime.ThreadContext;
4142
import org.jruby.runtime.Visibility;
4243
import org.jruby.runtime.builtin.IRubyObject;
4344
import org.jruby.util.ArraySupport;
4445
import org.jruby.util.ByteList;
4546
import org.jruby.util.Sprintf;
47+
import org.jruby.util.TypeConverter;
48+
import org.jruby.util.cli.Options;
4649

4750
/**
4851
* The Java representation of a Ruby NameError.
@@ -104,6 +107,113 @@ public IRubyObject dump(ThreadContext context, IRubyObject arg) {
104107

105108
@JRubyMethod
106109
public IRubyObject to_str(ThreadContext context) {
110+
if (Options.NAME_ERROR_INSPECT_OBJECT.load()) {
111+
// use old logic that inspects the target object
112+
return inspectToStr(context);
113+
}
114+
115+
String message = this.message;
116+
if (message == null) return context.nil;
117+
118+
final Ruby runtime = context.runtime;
119+
final IRubyObject object = this.object;
120+
121+
RubyString emptyFrozenString = runtime.getEmptyFrozenString();
122+
RubyString className, separator, description;
123+
className = separator = description = emptyFrozenString;
124+
125+
if (object == context.nil) {
126+
description = runtime.getNilString(); // "nil"
127+
} else if (object == context.tru) {
128+
description = runtime.getTrueString(); // "true"
129+
} else if (object == context.fals) {
130+
description = runtime.getFalseString(); // "false"
131+
} else {
132+
133+
// set up description
134+
if (message.contains("%2$s")) {
135+
description = getNameOrInspect(context, object);
136+
}
137+
138+
// set up separator text and class name
139+
IRubyObject classTmp = null;
140+
if (!object.isSpecialConst()) {
141+
if (object instanceof RubyClass) {
142+
separator = RubyString.newString(runtime, "class ");
143+
classTmp = object;
144+
} else if (object instanceof RubyModule) {
145+
separator = RubyString.newString(runtime, "module ");
146+
classTmp = object;
147+
}
148+
}
149+
150+
if (classTmp == null) {
151+
RubyClass klass = object.getMetaClass();
152+
if (klass.isSingleton()) {
153+
separator = RubyString.newString(runtime, "");
154+
if (object == runtime.getTopSelf()) {
155+
classTmp = RubyString.newString(runtime, "main");
156+
} else {
157+
classTmp = object.anyToString();
158+
}
159+
} else {
160+
separator = RubyString.newString(runtime, "an instance of ");
161+
classTmp = klass.getRealClass();
162+
}
163+
}
164+
165+
className = getNameOrInspect(context, classTmp);
166+
}
167+
168+
RubyArray arr = RubyArray.newArray(runtime, this.name, description, separator, className);
169+
170+
ByteList msgBytes = new ByteList(message.length() + description.size() + separator.size() + className.size(), USASCIIEncoding.INSTANCE);
171+
Sprintf.sprintf(msgBytes, message, arr);
172+
173+
return RubyString.newString(runtime, msgBytes);
174+
}
175+
176+
// MRI: coercion dance for name error object inspection in name_err_mesg_to_str
177+
private static RubyString getNameOrInspect(ThreadContext context, IRubyObject object) {
178+
IRubyObject tmp = tryModuleName(context, object);
179+
if (tmp == UNDEF || tmp.isNil()) tmp = tryInspect(context, object);
180+
if (tmp == UNDEF) context.setErrorInfo(context.nil);
181+
tmp = TypeConverter.checkStringType(context.runtime, tmp);
182+
if (tmp.isNil()) tmp = tmp.anyToString();
183+
return (RubyString) tmp;
184+
}
185+
186+
// MRI: rb_protect with rb_inspect callback
187+
private static IRubyObject tryInspect(ThreadContext context, IRubyObject object) {
188+
try {
189+
return RubyObject.inspect(context, object);
190+
} catch (RaiseException e) {
191+
// ignore
192+
}
193+
return UNDEF;
194+
}
195+
196+
// MRI: rb_protect with name_err_mesg_receiver_name callback
197+
private static IRubyObject tryModuleName(ThreadContext context, IRubyObject obj) {
198+
if (!obj.isSpecialConst() && obj instanceof RubyModule) {
199+
try {
200+
return Helpers.invokeChecked(context, obj, "name");
201+
} catch (RaiseException e) {
202+
// ignore
203+
}
204+
}
205+
return UNDEF;
206+
}
207+
208+
/**
209+
* Build the NameError message by inspecting the target object.
210+
*
211+
* Configurable by {@link Options#INSPECT_NAME_ERROR_OBJECT}. Removed in JRuby 10 to align with CRuby 3.4.
212+
*
213+
* @param context the current thread context
214+
* @return a string representing this NameError and its object
215+
*/
216+
private IRubyObject inspectToStr(ThreadContext context) {
107217
if (message == null) return context.nil;
108218

109219
final Ruby runtime = context.runtime;
@@ -119,7 +229,6 @@ public IRubyObject to_str(ThreadContext context) {
119229
description = RubyString.newStringShared(runtime, RubyBoolean.FALSE_BYTES); // "false"
120230
} else {
121231
try {
122-
// FIXME: MRI eagerly calculates name but #to_s and #inspect do not seem to do this call to name.
123232
if (object instanceof RubyModule && ((RubyModule) object).respondsTo("name")) {
124233
IRubyObject name = ((RubyModule) object).callMethod("name");
125234

@@ -148,8 +257,6 @@ public IRubyObject to_str(ThreadContext context) {
148257
className = separator = RubyString.newEmptyString(runtime);
149258
}
150259

151-
// RubyString name = this.name.asString(); // Symbol -> String
152-
153260
RubyArray arr = RubyArray.newArray(runtime, this.name, description, separator, className);
154261

155262
ByteList msgBytes = new ByteList(message.length() + description.size() + 16, USASCIIEncoding.INSTANCE); // name.size()

core/src/main/java/org/jruby/util/cli/Options.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public class Options {
165165
public static final Option<Boolean> REGEXP_INTERRUPTIBLE = bool(MISCELLANEOUS, "regexp.interruptible", true, "Allow regexp operations to be interruptible from Ruby.");
166166
public static final Option<Integer> JAR_CACHE_EXPIRATION = integer(MISCELLANEOUS, "jar.cache.expiration", 750, "The time (ms) between checks if a JAR file containing resources has been updated.");
167167
public static final Option<String> WINDOWS_FILESYSTEM_ENCODING = string(MISCELLANEOUS, "windows.filesystem.encoding", "UTF-8", "The encoding to use for filesystem names and paths on Windows.");
168-
public static final Option<Boolean> INSPECT_NAME_ERROR_OBJECT = bool(MISCELLANEOUS, "inspect.nameError.object", true, "Inspect the target object for display in NameError messages.");
168+
public static final Option<Boolean> NAME_ERROR_INSPECT_OBJECT = bool(MISCELLANEOUS, "nameError.inspect.object", true, "Inspect the target object for display in NameError messages.");
169169

170170
public static final Option<Boolean> DEBUG_LOADSERVICE = bool(DEBUG, "debug.loadService", false, "Log require/load file searches.");
171171
public static final Option<Boolean> DEBUG_LOADSERVICE_TIMING = bool(DEBUG, "debug.loadService.timing", false, "Log require/load parse+evaluate times.");

0 commit comments

Comments
 (0)