diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorElementNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorElementNode.java index 63103998..449b0c95 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorElementNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorElementNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.pkl.core.runtime.BaseModule; import org.pkl.core.runtime.VmClass; import org.pkl.core.runtime.VmDynamic; import org.pkl.core.runtime.VmListing; +import org.pkl.core.runtime.VmUtils; @ImportStatic(BaseModule.class) public abstract class GeneratorElementNode extends GeneratorMemberNode { @@ -63,6 +64,7 @@ public abstract class GeneratorElementNode extends GeneratorMemberNode { @SuppressWarnings("unused") void fallback(VirtualFrame frame, Object parent, ObjectData data) { CompilerDirectives.transferToInterpreter(); - throw exceptionBuilder().evalError("objectCannotHaveElement", parent).build(); + var parentClass = parent instanceof VmClass ? (VmClass) parent : VmUtils.getClass(parent); + throw exceptionBuilder().evalError("objectCannotHaveElement", parentClass).build(); } } diff --git a/pkl-core/src/main/java/org/pkl/core/util/ErrorMessages.java b/pkl-core/src/main/java/org/pkl/core/util/ErrorMessages.java index a17ff3dd..0c546408 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/ErrorMessages.java +++ b/pkl-core/src/main/java/org/pkl/core/util/ErrorMessages.java @@ -20,10 +20,14 @@ import java.util.Locale; import java.util.ResourceBundle; import java.util.stream.*; import org.jspecify.annotations.Nullable; +import org.pkl.core.runtime.VmValue; +import org.pkl.core.runtime.VmValueRenderer; public final class ErrorMessages { private ErrorMessages() {} + private static final VmValueRenderer renderer = VmValueRenderer.singleLine(Integer.MAX_VALUE); + public static String create(String messageName, @Nullable Object... args) { var locale = Locale.getDefault(); String errorMessage = @@ -33,7 +37,18 @@ public final class ErrorMessages { if (args.length == 0) return errorMessage; var formatter = new MessageFormat(errorMessage, locale); - return formatter.format(args); + // TODO: we render VmValues here with VmValueRenderer, but that's not enough to properly + // render all kinds of values, like Pkl Strings, for example + @Nullable Object[] actualArgs = new @Nullable Object[args.length]; + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + if (arg instanceof VmValue) { + actualArgs[i] = renderer.render(arg); + } else { + actualArgs[i] = arg; + } + } + return formatter.format(actualArgs); } public static String createIndented(String messageName, String indent, @Nullable Object... args) { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/listings2/wrongParent.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/listings2/wrongParent.pcf index 626cc23f..b15604fa 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/listings2/wrongParent.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/listings2/wrongParent.pcf @@ -1,12 +1,12 @@ res1 = "Cannot instantiate, or amend an instance of, external class `Int`." res2 = "Cannot instantiate, or amend an instance of, external class `List`." res3 = "Cannot instantiate, or amend an instance of, external class `List`." -res4 = "Object of type `new Person {}` cannot have an element." +res4 = "Object of type `wrongParent#Person` cannot have an element." res5 = "Cannot instantiate abstract class `ValueRenderer`." -res6 = "Object of type `new Mapping {}` cannot have an element." +res6 = "Object of type `Mapping` cannot have an element." res7 = "Cannot instantiate, or amend an instance of, external class `Int`." res8 = "Cannot instantiate, or amend an instance of, external class `List`." res9 = "Cannot instantiate, or amend an instance of, external class `List`." -res10 = "Object of type `new Person {}` cannot have an element." +res10 = "Object of type `wrongParent#Person` cannot have an element." res11 = "Cannot instantiate abstract class `ValueRenderer`." -res12 = "Object of type `new Mapping {}` cannot have an element." +res12 = "Object of type `Mapping` cannot have an element." diff --git a/pkl-core/src/test/kotlin/org/pkl/core/util/ErrorMessagesTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/util/ErrorMessagesTest.kt new file mode 100644 index 00000000..14fe1cee --- /dev/null +++ b/pkl-core/src/test/kotlin/org/pkl/core/util/ErrorMessagesTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.util + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Test +import org.pkl.core.runtime.VmClass +import org.pkl.core.runtime.VmValue +import org.pkl.core.runtime.VmValueConverter +import org.pkl.core.runtime.VmValueVisitor + +class ErrorMessagesTest { + private class UnforceableValue : VmValue() { + var forced = false + private set + + override fun force(allowUndefinedValues: Boolean) { + forced = true + throw RuntimeException("value must not be forced while rendering an error message") + } + + override fun accept(visitor: VmValueVisitor) { + visitor.visitString("lazy") + } + + override fun accept(converter: VmValueConverter, path: Iterable): T = + throw UnsupportedOperationException() + + override fun equals(obj: Any?): Boolean = this === obj + + override fun toString(): String { + force(true) + return "lazy" + } + + override fun getVmClass(): VmClass = throw UnsupportedOperationException() + + override fun export(): Any = throw UnsupportedOperationException() + + override fun hashCode(): Int = System.identityHashCode(this) + } + + @Test + fun `renders VmValue arguments without forcing them`() { + val value = UnforceableValue() + + lateinit var message: String + assertThatCode { message = ErrorMessages.create("cannotIterateOverThisValue", value) } + .doesNotThrowAnyException() + + assertThat(value.forced).isFalse() + assertThat(message).contains("lazy") + } +}