This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new a95a12691 fix(java): ignore non-Scala/Lombok-style default helper
methods (#3733)
a95a12691 is described below
commit a95a12691a58fead31dcf9017317e3026fe53718
Author: Sebastian Mandrean <[email protected]>
AuthorDate: Thu Jun 4 00:22:03 2026 +0200
fix(java): ignore non-Scala/Lombok-style default helper methods (#3733)
## Why?
When Scala is present on the runtime classpath, Java compatible
serializers enable Scala default-value support globally. Plain Java
classes generated with Lombok `@Builder.Default` can contain private
helper methods named like `$default$imageRelations()`, which previously
matched the broad Scala default-method scan and caused warning noise
during deserialization.
## What does this PR do?
- Restricts Scala default-value method detection to known Scala
constructor/apply helper prefixes with numeric parameter suffixes.
- Ignores non-Scala/Lombok-style `$default$` helper names and
out-of-range parameter indexes without invoking them.
- Keeps Scala case-class and regular-class constructor defaults working,
including nested/default-value regression coverage.
- Adds Java fixtures and Scala serializer tests covering Lombok-like
helper methods.
## Related issues
Closes #3720
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A. This is a targeted default-method filtering fix and does not change
serialization format or hot-path serialization logic.
## Validation
- `mvn -T16 -pl fory-core -am install -DskipTests
-Dmaven.javadoc.skip=true -Dmaven.source.skip=true`
- `mvn -pl fory-core
-Dtest=org.apache.fory.platform.AndroidSupportStaticCheckTest#testScalaDefaultValuesDoNotUseTrustedLookupOnAndroid
test`
- `sbt "+testOnly org.apache.fory.util.ScalaDefaultValueUtilsTest
org.apache.fory.serializer.scala.ScalaDefaultValueTest"`
- `sbt +test`
- `mvn -T16 spotless:check checkstyle:check`
Co-authored-by: Shawn Yang <[email protected]>
---
.../org/apache/fory/util/DefaultValueUtils.java | 182 +++++++++++++--------
.../platform/AndroidSupportStaticCheckTest.java | 29 ++--
.../scala/LombokDefaultLikeJavaPojo.java | 79 +++++++++
.../fory/util/ConstructorDefaultLikeJavaClass.java | 40 +++++
.../fory/util/LombokDefaultLikeJavaClass.java | 61 +++++++
.../serializer/scala/ScalaDefaultValueTest.scala | 12 +-
.../fory/util/ScalaDefaultValueUtilsTest.scala | 50 ++++++
7 files changed, 375 insertions(+), 78 deletions(-)
diff --git
a/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java
b/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java
index e5d43cc9a..77ff2d540 100644
--- a/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java
@@ -50,6 +50,9 @@ import org.apache.fory.type.Types;
@Internal
public class DefaultValueUtils {
private static final Logger LOG =
LoggerFactory.getLogger(DefaultValueUtils.class);
+ private static final String SCALA_APPLY_DEFAULT_METHOD_PREFIX =
"apply$default$";
+ private static final String SCALA_CONSTRUCTOR_DEFAULT_METHOD_PREFIX =
+ "$lessinit$greater$default$";
private static final ClassValueCache<Map<Integer, Object>>
cachedCtrDefaultValues =
ClassValueCache.newClassKeySoftCache(32);
@@ -226,8 +229,8 @@ public class DefaultValueUtils {
}
Preconditions.checkNotNull(
primaryConstructor, "Primary constructor not found for class " +
cls.getName());
- Map<Integer, Object> defaultValues = getDefaultValuesForClass(cls);
int paramCount = primaryConstructor.getParameterCount();
+ Map<Integer, Object> defaultValues = getDefaultValuesForClass(cls,
paramCount);
for (int i = 0; i < paramCount; i++) {
String paramName = primaryConstructor.getParameters()[i].getName();
Object defaultValue = defaultValues.get(i + 1); // +1 because default
values are 1-indexed
@@ -245,75 +248,42 @@ public class DefaultValueUtils {
* @param cls the Scala class
* @return a map from parameter index to method handle
*/
- private Map<Integer, Object> getDefaultValuesForClass(Class<?> cls) {
+ private Map<Integer, Object> getDefaultValuesForClass(Class<?> cls, int
paramCount) {
if (cachedCtrDefaultValues.getIfPresent(cls) != null) {
return cachedCtrDefaultValues.getIfPresent(cls);
}
Map<Integer, Object> defaultValueMethods;
if (ScalaTypes.isScalaProductType(cls)) {
- defaultValueMethods = getDefaultValuesForCaseClass(cls);
+ defaultValueMethods = getDefaultValuesForCaseClass(cls, paramCount);
} else {
- defaultValueMethods = getDefaultValuesForRegularScalaClass(cls);
+ defaultValueMethods = getDefaultValuesForRegularScalaClass(cls,
paramCount);
}
cachedCtrDefaultValues.put(cls, defaultValueMethods);
return defaultValueMethods;
}
- private static Map<Integer, Object> getDefaultValuesForCaseClass(Class<?>
cls) {
+ private static Map<Integer, Object> getDefaultValuesForCaseClass(Class<?>
cls, int paramCount) {
Map<Integer, Object> values = new HashMap<>();
- String companionClassName = cls.getName() + "$";
- Class<?> companionClass = null;
- Object companionInstance = null;
- try {
- companionClass = Class.forName(companionClassName, false,
cls.getClassLoader());
- companionInstance = companionClass.getField("MODULE$").get(null);
- } catch (Exception e) {
- // For nested case classes, try to find the companion object in the
enclosing class
- Class<?> enclosingClass = cls.getEnclosingClass();
- if (enclosingClass != null) {
- // Look for a companion object field in the enclosing class
- for (java.lang.reflect.Field field :
enclosingClass.getDeclaredFields()) {
- if (field.getType().getName().equals(companionClassName)) {
- field.setAccessible(true);
- try {
- companionInstance = field.get(null);
- } catch (Exception e1) {
- LOG.warn(
- "Error {} accessing companion object for {}, default
values support is disabled when deserializing object of type {}",
- e1.getMessage(),
- cls.getName(),
- cls.getName());
- return values;
- }
- if (companionInstance != null) {
- companionClass = companionInstance.getClass();
- break;
- }
- }
- }
- }
- }
- if (companionClass == null) {
- LOG.warn(
- "Companion class not found for {}, default values support is
disabled when deserializing object of type {}",
- cls.getName(),
- cls.getName());
+ ScalaCompanionObject companionObject = getScalaCompanionObject(cls,
true);
+ if (companionObject == null) {
return values;
}
MethodHandles.Lookup lookup =
- AndroidSupport.IS_ANDROID ? null :
_JDKAccess._trustedLookup(companionClass);
+ AndroidSupport.IS_ANDROID ? null :
_JDKAccess._trustedLookup(companionObject.cls);
- // Look for methods named `apply$default$1`, `apply$default$2`, etc.
- Method[] companionMethods = companionClass.getDeclaredMethods();
+ Method[] companionMethods = companionObject.cls.getDeclaredMethods();
for (Method method : companionMethods) {
- String methodName = method.getName();
- if (methodName.contains("$default$")) {
+ int paramIndex =
+ getScalaDefaultParameterIndex(method,
SCALA_APPLY_DEFAULT_METHOD_PREFIX, paramCount);
+ if (paramIndex < 0) {
+ paramIndex =
+ getScalaDefaultParameterIndex(
+ method, SCALA_CONSTRUCTOR_DEFAULT_METHOD_PREFIX, paramCount);
+ }
+ if (paramIndex > 0) {
try {
- // Extract the parameter index from the method name
- String indexStr =
- methodName.substring(methodName.lastIndexOf("$default$") +
"$default$".length());
- int paramIndex = Integer.parseInt(indexStr);
- Object defaultValue = invokeDefaultValueMethod(lookup, method,
companionInstance);
+ Object defaultValue =
+ invokeDefaultValueMethod(lookup, method,
companionObject.instance);
values.put(paramIndex, defaultValue);
} catch (Throwable e) {
LOG.warn(
@@ -328,23 +298,25 @@ public class DefaultValueUtils {
return values;
}
- private static Map<Integer, Object>
getDefaultValuesForRegularScalaClass(Class<?> cls) {
+ private static Map<Integer, Object> getDefaultValuesForRegularScalaClass(
+ Class<?> cls, int paramCount) {
Map<Integer, Object> values = new HashMap<>();
+ ScalaCompanionObject companionObject = getScalaCompanionObject(cls,
false);
+ if (companionObject == null) {
+ return values;
+ }
try {
MethodHandles.Lookup lookup =
- AndroidSupport.IS_ANDROID ? null : _JDKAccess._trustedLookup(cls);
- Method[] classMethods = cls.getDeclaredMethods();
- for (Method method : classMethods) {
- String methodName = method.getName();
- if (methodName.contains("$default$")) {
+ AndroidSupport.IS_ANDROID ? null :
_JDKAccess._trustedLookup(companionObject.cls);
+ Method[] companionMethods = companionObject.cls.getDeclaredMethods();
+ for (Method method : companionMethods) {
+ int paramIndex =
+ getScalaDefaultParameterIndex(
+ method, SCALA_CONSTRUCTOR_DEFAULT_METHOD_PREFIX, paramCount);
+ if (paramIndex > 0) {
try {
- // Extract the parameter index from the method name
- String indexStr =
- methodName.substring(methodName.lastIndexOf("$default$") +
"$default$".length());
- int paramIndex = Integer.parseInt(indexStr);
- // For regular Scala classes, we need to create an instance to
call instance methods
- // Since these are default value methods, we can try to call
them as static methods
- Object defaultValue = invokeDefaultValueMethod(lookup, method,
null);
+ Object defaultValue =
+ invokeDefaultValueMethod(lookup, method,
companionObject.instance);
values.put(paramIndex, defaultValue);
} catch (Throwable e) {
LOG.warn(
@@ -367,6 +339,48 @@ public class DefaultValueUtils {
return values;
}
+ private static ScalaCompanionObject getScalaCompanionObject(
+ Class<?> cls, boolean warnIfMissing) {
+ String companionClassName = cls.getName() + "$";
+ try {
+ Class<?> companionClass = Class.forName(companionClassName, false,
cls.getClassLoader());
+ Object companionInstance =
companionClass.getField("MODULE$").get(null);
+ if (companionInstance != null) {
+ return new ScalaCompanionObject(companionClass, companionInstance);
+ }
+ } catch (Exception e) {
+ // For nested case classes, try to find the companion object in the
enclosing class.
+ Class<?> enclosingClass = cls.getEnclosingClass();
+ if (enclosingClass != null) {
+ for (java.lang.reflect.Field field :
enclosingClass.getDeclaredFields()) {
+ if (field.getType().getName().equals(companionClassName)) {
+ field.setAccessible(true);
+ try {
+ Object companionInstance = field.get(null);
+ if (companionInstance != null) {
+ return new
ScalaCompanionObject(companionInstance.getClass(), companionInstance);
+ }
+ } catch (Exception e1) {
+ LOG.warn(
+ "Error {} accessing companion object for {}, default
values support is disabled when deserializing object of type {}",
+ e1.getMessage(),
+ cls.getName(),
+ cls.getName());
+ return null;
+ }
+ }
+ }
+ }
+ }
+ if (warnIfMissing) {
+ LOG.warn(
+ "Companion class not found for {}, default values support is
disabled when deserializing object of type {}",
+ cls.getName(),
+ cls.getName());
+ }
+ return null;
+ }
+
private static Object invokeDefaultValueMethod(
MethodHandles.Lookup lookup, Method method, Object target) throws
Throwable {
if (AndroidSupport.IS_ANDROID) {
@@ -383,6 +397,44 @@ public class DefaultValueUtils {
}
return methodHandle.invoke(target);
}
+
+ private static int getScalaDefaultParameterIndex(
+ Method method, String methodPrefix, int maxParamIndex) {
+ if (method.getParameterCount() != 0) {
+ return -1;
+ }
+ String methodName = method.getName();
+ if (!methodName.startsWith(methodPrefix)) {
+ return -1;
+ }
+ int indexStart = methodPrefix.length();
+ if (indexStart == methodName.length()) {
+ return -1;
+ }
+ int paramIndex = 0;
+ for (int i = indexStart; i < methodName.length(); i++) {
+ char c = methodName.charAt(i);
+ if (c < '0' || c > '9') {
+ return -1;
+ }
+ int digit = c - '0';
+ if (paramIndex > (Integer.MAX_VALUE - digit) / 10) {
+ return -1;
+ }
+ paramIndex = paramIndex * 10 + digit;
+ }
+ return paramIndex > 0 && paramIndex <= maxParamIndex ? paramIndex : -1;
+ }
+
+ private static final class ScalaCompanionObject {
+ private final Class<?> cls;
+ private final Object instance;
+
+ private ScalaCompanionObject(Class<?> cls, Object instance) {
+ this.cls = cls;
+ this.instance = instance;
+ }
+ }
}
/** Convert value to correct type based on dispatchId. */
diff --git
a/java/fory-core/src/test/java/org/apache/fory/platform/AndroidSupportStaticCheckTest.java
b/java/fory-core/src/test/java/org/apache/fory/platform/AndroidSupportStaticCheckTest.java
index 47066b0da..8d1797850 100644
---
a/java/fory-core/src/test/java/org/apache/fory/platform/AndroidSupportStaticCheckTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/platform/AndroidSupportStaticCheckTest.java
@@ -28,6 +28,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.testng.annotations.Test;
@@ -42,15 +43,12 @@ public class AndroidSupportStaticCheckTest {
private static final Pattern ANDROID_GATED_FIELD_GET_ANNOTATED_TYPE =
Pattern.compile(
"AndroidSupport\\.IS_ANDROID[\\s\\S]{0,160}\\.getAnnotatedType\\s*\\(",
Pattern.DOTALL);
- private static final Pattern ANDROID_GATED_SCALA_COMPANION_LOOKUP =
+ private static final Pattern SCALA_TRUSTED_LOOKUP =
+ Pattern.compile("_JDKAccess\\._trustedLookup\\(");
+ private static final Pattern ANDROID_GATED_SCALA_TRUSTED_LOOKUP =
Pattern.compile(
"AndroidSupport\\.IS_ANDROID\\s*\\?\\s*null\\s*:\\s*"
- + "_JDKAccess\\._trustedLookup\\(companionClass\\)",
- Pattern.DOTALL);
- private static final Pattern ANDROID_GATED_SCALA_CLASS_LOOKUP =
- Pattern.compile(
- "AndroidSupport\\.IS_ANDROID\\s*\\?\\s*null\\s*:\\s*"
- + "_JDKAccess\\._trustedLookup\\(cls\\)",
+ + "_JDKAccess\\._trustedLookup\\([^)]*\\)",
Pattern.DOTALL);
private static final Pattern ANDROID_REFLECTIVE_SCALA_DEFAULT_INVOKE =
Pattern.compile(
@@ -155,12 +153,19 @@ public class AndroidSupportStaticCheckTest {
public void testScalaDefaultValuesDoNotUseTrustedLookupOnAndroid() throws
IOException {
Path sourcePath =
Paths.get("src/main/java/org/apache/fory/util/DefaultValueUtils.java");
String source = new String(Files.readAllBytes(sourcePath),
StandardCharsets.UTF_8);
+ List<String> violations = new ArrayList<>();
+ Matcher matcher = SCALA_TRUSTED_LOOKUP.matcher(source);
+ while (matcher.find()) {
+ int start = Math.max(0, matcher.start() - 120);
+ int end = Math.min(source.length(), matcher.end() + 160);
+ String context = source.substring(start, end);
+ if (!ANDROID_GATED_SCALA_TRUSTED_LOOKUP.matcher(context).find()) {
+ violations.add(context);
+ }
+ }
assertTrue(
- ANDROID_GATED_SCALA_COMPANION_LOOKUP.matcher(source).find(),
- "Scala companion default values must not use _JDKAccess trusted lookup
on Android");
- assertTrue(
- ANDROID_GATED_SCALA_CLASS_LOOKUP.matcher(source).find(),
- "Regular Scala default values must not use _JDKAccess trusted lookup
on Android");
+ violations.isEmpty(),
+ "Scala default values must not use _JDKAccess trusted lookup on
Android: " + violations);
assertTrue(
ANDROID_REFLECTIVE_SCALA_DEFAULT_INVOKE.matcher(source).find(),
"Android Scala default values must invoke default methods through
direct reflection");
diff --git
a/scala/src/test/java/org/apache/fory/serializer/scala/LombokDefaultLikeJavaPojo.java
b/scala/src/test/java/org/apache/fory/serializer/scala/LombokDefaultLikeJavaPojo.java
new file mode 100644
index 000000000..53038b125
--- /dev/null
+++
b/scala/src/test/java/org/apache/fory/serializer/scala/LombokDefaultLikeJavaPojo.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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.apache.fory.serializer.scala;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class LombokDefaultLikeJavaPojo {
+ private String id;
+ private List<String> imageRelations;
+
+ public LombokDefaultLikeJavaPojo() {
+ this(null, Collections.emptyList());
+ }
+
+ public LombokDefaultLikeJavaPojo(String id, List<String> imageRelations) {
+ this.id = id;
+ this.imageRelations = imageRelations;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public List<String> getImageRelations() {
+ return imageRelations;
+ }
+
+ private static List<String> $default$imageRelations() {
+ return Collections.emptyList();
+ }
+
+ private static List<String> $default$1() {
+ return Collections.emptyList();
+ }
+
+ private List<String> $default$2() {
+ return Collections.emptyList();
+ }
+
+ private static List<String> $default$99() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof LombokDefaultLikeJavaPojo)) {
+ return false;
+ }
+ LombokDefaultLikeJavaPojo that = (LombokDefaultLikeJavaPojo) o;
+ return Objects.equals(id, that.id) && Objects.equals(imageRelations,
that.imageRelations);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, imageRelations);
+ }
+}
diff --git
a/scala/src/test/java/org/apache/fory/util/ConstructorDefaultLikeJavaClass.java
b/scala/src/test/java/org/apache/fory/util/ConstructorDefaultLikeJavaClass.java
new file mode 100644
index 000000000..142362da2
--- /dev/null
+++
b/scala/src/test/java/org/apache/fory/util/ConstructorDefaultLikeJavaClass.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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.apache.fory.util;
+
+public class ConstructorDefaultLikeJavaClass {
+ private final String id;
+
+ public ConstructorDefaultLikeJavaClass(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ private static String $lessinit$greater$default$1() {
+ return "default-id";
+ }
+
+ private static String $lessinit$greater$default$2() {
+ return "out-of-range";
+ }
+}
diff --git
a/scala/src/test/java/org/apache/fory/util/LombokDefaultLikeJavaClass.java
b/scala/src/test/java/org/apache/fory/util/LombokDefaultLikeJavaClass.java
new file mode 100644
index 000000000..177f269a3
--- /dev/null
+++ b/scala/src/test/java/org/apache/fory/util/LombokDefaultLikeJavaClass.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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.apache.fory.util;
+
+import java.util.Collections;
+import java.util.List;
+
+public class LombokDefaultLikeJavaClass {
+ private String id;
+ private List<String> imageRelations;
+
+ public LombokDefaultLikeJavaClass() {
+ this(null, Collections.emptyList());
+ }
+
+ public LombokDefaultLikeJavaClass(String id, List<String> imageRelations) {
+ this.id = id;
+ this.imageRelations = imageRelations;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public List<String> getImageRelations() {
+ return imageRelations;
+ }
+
+ private static List<String> $default$imageRelations() {
+ return Collections.emptyList();
+ }
+
+ private static List<String> $default$1() {
+ return Collections.emptyList();
+ }
+
+ private List<String> $default$2() {
+ return Collections.emptyList();
+ }
+
+ private static List<String> $default$99() {
+ return Collections.emptyList();
+ }
+}
diff --git
a/scala/src/test/scala/org/apache/fory/serializer/scala/ScalaDefaultValueTest.scala
b/scala/src/test/scala/org/apache/fory/serializer/scala/ScalaDefaultValueTest.scala
index 95a3c82e0..5236309f0 100644
---
a/scala/src/test/scala/org/apache/fory/serializer/scala/ScalaDefaultValueTest.scala
+++
b/scala/src/test/scala/org/apache/fory/serializer/scala/ScalaDefaultValueTest.scala
@@ -23,6 +23,7 @@ import org.apache.fory.Fory
import org.apache.fory.scala.ForyScala
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
+import java.util.Arrays
object NestedCases {
// Classes WITHOUT default values for serialization
@@ -182,6 +183,15 @@ class ScalaDefaultValueTest extends AnyWordSpec with
Matchers {
deserialized.age shouldEqual 25 // Should use default value
deserialized.city shouldEqual "Unknown" // Should use default value
}
+
+ s"ignore Java Lombok-style default helper methods in $modeName" in {
+ val fory = createFory(codegen)
+ val original = new LombokDefaultLikeJavaPojo("1",
Arrays.asList("thumbnail", "hero"))
+ val deserialized =
+ fory.deserialize(fory.serialize(original),
classOf[LombokDefaultLikeJavaPojo])
+
+ deserialized shouldEqual original
+ }
}
}
}
@@ -194,4 +204,4 @@ case class CaseClassComplexDefaultsNoDefaults(v: String) //
No list field at all
// Test case classes WITH default values (for deserialization)
case class CaseClassWithDefaults(v: String, x: Int = 1)
case class CaseClassMultipleDefaultsWithDefaults(v: String, x: Int = 1, y:
Double = 2.0)
-case class CaseClassComplexDefaultsWithDefaults(v: String, list: List[Int] =
List(1, 2, 3))
\ No newline at end of file
+case class CaseClassComplexDefaultsWithDefaults(v: String, list: List[Int] =
List(1, 2, 3))
diff --git
a/scala/src/test/scala/org/apache/fory/util/ScalaDefaultValueUtilsTest.scala
b/scala/src/test/scala/org/apache/fory/util/ScalaDefaultValueUtilsTest.scala
index ea4cc541f..aae7dab8f 100644
--- a/scala/src/test/scala/org/apache/fory/util/ScalaDefaultValueUtilsTest.scala
+++ b/scala/src/test/scala/org/apache/fory/util/ScalaDefaultValueUtilsTest.scala
@@ -26,7 +26,10 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.Succeeded
import org.scalatest.matchers.should.Matchers._
+import java.io.{ByteArrayOutputStream, PrintStream}
+import java.nio.charset.StandardCharsets
import java.util.{List => JavaList, ArrayList}
+import org.apache.fory.logging.{LogLevel, LoggerFactory}
import scala.jdk.CollectionConverters._
// Test case classes WITH default values for testing DefaultValueUtils
@@ -122,6 +125,18 @@ class TestJavaClass(val name: String, val age: Int) {
override def toString: String = s"TestJavaClass($name, $age)"
}
+class TestRegularScalaClassMethodDefaultsOnly(val name: String) {
+ def helper(limit: Int = 10): Int = limit
+}
+
+class TestRegularScalaClassOutOfRangeCompanionDefault(val name: String)
+
+object TestRegularScalaClassOutOfRangeCompanionDefault {
+ def `$lessinit$greater$default$2`(): String = {
+ throw new AssertionError("out-of-range constructor default helper should
not be invoked")
+ }
+}
+
// Object to contain truly nested case classes
object NestedClasses {
case class NestedCaseClass(
@@ -170,8 +185,43 @@ class ScalaDefaultValueUtilsTest extends AnyWordSpec with
Matchers {
scalaDefaultValueSupport().buildDefaultValueFields(fory, cls, descriptors)
}
+ def assertNoScalaDefaultValuesOrWarnings(classes: Class[_]*): Unit = {
+ val output = new ByteArrayOutputStream()
+ val previousOut = System.out
+ val previousLogLevel = LoggerFactory.getLogLevel
+ val capture = new PrintStream(output, true, StandardCharsets.UTF_8.name())
+ try {
+ System.setOut(capture)
+ LoggerFactory.setLogLevel(LogLevel.WARN_LEVEL)
+
+ classes.foreach { cls =>
+ withClue(s"${cls.getName}: ") {
+ scalaDefaultValueSupport().hasDefaultValues(cls) shouldEqual false
+ scalaDefaultValueSupport().getAllDefaultValues(cls).asScala shouldBe
empty
+ buildDefaultValueFields(cls) shouldBe empty
+ }
+ }
+ } finally {
+ LoggerFactory.setLogLevel(previousLogLevel)
+ System.setOut(previousOut)
+ capture.close()
+ }
+
+ val logs = new String(output.toByteArray, StandardCharsets.UTF_8)
+ logs should not include "WARN DefaultValueUtils"
+ logs should not include "finding default value"
+ }
+
"DefaultValueUtils" should {
+ "ignore non-constructor default-like methods without warnings" in {
+ assertNoScalaDefaultValuesOrWarnings(
+ classOf[LombokDefaultLikeJavaClass],
+ classOf[ConstructorDefaultLikeJavaClass],
+ classOf[TestRegularScalaClassMethodDefaultsOnly],
+ classOf[TestRegularScalaClassOutOfRangeCompanionDefault])
+ }
+
"detect Scala classes with default values correctly" in {
// Test case classes with default values
scalaDefaultValueSupport().hasDefaultValues(classOf[TestCaseClassWithDefaults])
shouldEqual true
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]