Skip to content

Commit

Permalink
Add JRuby 10 inspectless logic for NameError
Browse files Browse the repository at this point in the history
Configure with -Xinspect.nameError.object=true|false. It defaults
to true in JRuby 9.4.10.0 and false in JRuby 10.
  • Loading branch information
headius committed Dec 20, 2024
1 parent 353aa59 commit 818fbc6
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 4 deletions.
113 changes: 110 additions & 3 deletions core/src/main/java/org/jruby/RubyNameError.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@
import org.jruby.exceptions.NameError;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.Block;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ArraySupport;
import org.jruby.util.ByteList;
import org.jruby.util.Sprintf;
import org.jruby.util.TypeConverter;
import org.jruby.util.cli.Options;

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

@JRubyMethod
public IRubyObject to_str(ThreadContext context) {
if (Options.NAME_ERROR_INSPECT_OBJECT.load()) {
// use old logic that inspects the target object
return inspectToStr(context);
}

String message = this.message;
if (message == null) return context.nil;

final Ruby runtime = context.runtime;
final IRubyObject object = this.object;

RubyString emptyFrozenString = runtime.getEmptyFrozenString();
RubyString className, separator, description;
className = separator = description = emptyFrozenString;

if (object == context.nil) {
description = runtime.getNilString(); // "nil"
} else if (object == context.tru) {
description = runtime.getTrueString(); // "true"
} else if (object == context.fals) {
description = runtime.getFalseString(); // "false"
} else {

// set up description
if (message.contains("%2$s")) {
description = getNameOrInspect(context, object);
}

// set up separator text and class name
IRubyObject classTmp = null;
if (!object.isSpecialConst()) {
if (object instanceof RubyClass) {
separator = RubyString.newString(runtime, "class ");
classTmp = object;
} else if (object instanceof RubyModule) {
separator = RubyString.newString(runtime, "module ");
classTmp = object;
}
}

if (classTmp == null) {
RubyClass klass = object.getMetaClass();
if (klass.isSingleton()) {
separator = RubyString.newString(runtime, "");
if (object == runtime.getTopSelf()) {
classTmp = RubyString.newString(runtime, "main");
} else {
classTmp = object.anyToString();
}
} else {
separator = RubyString.newString(runtime, "an instance of ");
classTmp = klass.getRealClass();
}
}

className = getNameOrInspect(context, classTmp);
}

RubyArray arr = RubyArray.newArray(runtime, this.name, description, separator, className);

ByteList msgBytes = new ByteList(message.length() + description.size() + separator.size() + className.size(), USASCIIEncoding.INSTANCE);
Sprintf.sprintf(msgBytes, message, arr);

return RubyString.newString(runtime, msgBytes);
}

// MRI: coercion dance for name error object inspection in name_err_mesg_to_str
private static RubyString getNameOrInspect(ThreadContext context, IRubyObject object) {
IRubyObject tmp = tryModuleName(context, object);
if (tmp == UNDEF || tmp.isNil()) tmp = tryInspect(context, object);
if (tmp == UNDEF) context.setErrorInfo(context.nil);
tmp = TypeConverter.checkStringType(context.runtime, tmp);
if (tmp.isNil()) tmp = tmp.anyToString();
return (RubyString) tmp;
}

// MRI: rb_protect with rb_inspect callback
private static IRubyObject tryInspect(ThreadContext context, IRubyObject object) {
try {
return RubyObject.inspect(context, object);
} catch (RaiseException e) {
// ignore
}
return UNDEF;
}

// MRI: rb_protect with name_err_mesg_receiver_name callback
private static IRubyObject tryModuleName(ThreadContext context, IRubyObject obj) {
if (!obj.isSpecialConst() && obj instanceof RubyModule) {
try {
return Helpers.invokeChecked(context, obj, "name");
} catch (RaiseException e) {
// ignore
}
}
return UNDEF;
}

/**
* Build the NameError message by inspecting the target object.
*
* Configurable by {@link Options#INSPECT_NAME_ERROR_OBJECT}. Removed in JRuby 10 to align with CRuby 3.4.
*
* @param context the current thread context
* @return a string representing this NameError and its object
*/
private IRubyObject inspectToStr(ThreadContext context) {
if (message == null) return context.nil;

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

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

// RubyString name = this.name.asString(); // Symbol -> String

RubyArray arr = RubyArray.newArray(runtime, this.name, description, separator, className);

ByteList msgBytes = new ByteList(message.length() + description.size() + 16, USASCIIEncoding.INSTANCE); // name.size()
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/util/cli/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public class Options {
public static final Option<Boolean> REGEXP_INTERRUPTIBLE = bool(MISCELLANEOUS, "regexp.interruptible", true, "Allow regexp operations to be interruptible from Ruby.");
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.");
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.");
public static final Option<Boolean> INSPECT_NAME_ERROR_OBJECT = bool(MISCELLANEOUS, "inspect.nameError.object", true, "Inspect the target object for display in NameError messages.");
public static final Option<Boolean> NAME_ERROR_INSPECT_OBJECT = bool(MISCELLANEOUS, "nameError.inspect.object", true, "Inspect the target object for display in NameError messages.");

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

0 comments on commit 818fbc6

Please sign in to comment.