From 818fbc6f15846a32e8023ced5462ad688c6dc45a Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Thu, 19 Dec 2024 17:33:52 -0600 Subject: [PATCH] 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. --- .../main/java/org/jruby/RubyNameError.java | 113 +++++++++++++++++- .../main/java/org/jruby/util/cli/Options.java | 2 +- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/jruby/RubyNameError.java b/core/src/main/java/org/jruby/RubyNameError.java index bdb59285c9c..1089df7f486 100644 --- a/core/src/main/java/org/jruby/RubyNameError.java +++ b/core/src/main/java/org/jruby/RubyNameError.java @@ -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. @@ -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; @@ -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"); @@ -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() diff --git a/core/src/main/java/org/jruby/util/cli/Options.java b/core/src/main/java/org/jruby/util/cli/Options.java index 68af60cf52d..01d4c314fc2 100644 --- a/core/src/main/java/org/jruby/util/cli/Options.java +++ b/core/src/main/java/org/jruby/util/cli/Options.java @@ -165,7 +165,7 @@ public class Options { public static final Option REGEXP_INTERRUPTIBLE = bool(MISCELLANEOUS, "regexp.interruptible", true, "Allow regexp operations to be interruptible from Ruby."); public static final Option 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 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 INSPECT_NAME_ERROR_OBJECT = bool(MISCELLANEOUS, "inspect.nameError.object", true, "Inspect the target object for display in NameError messages."); + public static final Option NAME_ERROR_INSPECT_OBJECT = bool(MISCELLANEOUS, "nameError.inspect.object", true, "Inspect the target object for display in NameError messages."); public static final Option DEBUG_LOADSERVICE = bool(DEBUG, "debug.loadService", false, "Log require/load file searches."); public static final Option DEBUG_LOADSERVICE_TIMING = bool(DEBUG, "debug.loadService.timing", false, "Log require/load parse+evaluate times.");