Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/json/scala.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ or mutate constructor `val` fields. Defaults in later parameter lists receive th
constructor arguments exactly as Scala defines them. A missing parameter without a default is an
error. Mutable body properties are applied after construction.

A case class may be declared at the top level, or inside an `object` at any nesting depth, as long
as every enclosing scope is itself an `object`. A case class enclosed by a `class`, a trait, or a
method is rejected for both reading and writing, because Fory cannot reach the enclosing instance
or the companion it needs to rebuild the value.

Fory JSON annotations can be placed directly on Scala constructor properties:

```scala
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,8 +630,14 @@ public static String getLiteralName(Class<?> cls) {
// qualifier name of scala object type will ends with `.`
canonicalName = clsName.substring(0, clsName.length() - 1).replace("$", ".") + "$";
} else {
if (!canonicalName.endsWith("$") && canonicalName.contains("$")) {
// nested scala object type can't be accessed in java by using canonicalName
if (canonicalName.contains("$")) {
// nested scala object type can't be accessed in java by using canonicalName. This includes
// a nested module class, whose own name ends with `$`: the canonical name of a companion
// declared two or more levels inside an object has a `$`-terminated segment in the
// middle of its canonical name, one per enclosing module class, and the generated-code
// compiler cannot resolve through those ("pkg.A$B$ declares no member type C$"). One level
// deep is enclosed by the mirror class instead, so `pkg.A.C$` ends with the only `$` and
// still resolves: the nesting-level bound below is load bearing in both directions.
// see more detailed in
// https://stackoverflow.com/questions/30809070/accessing-scala-nested-classes-from-java
int nestedLevels = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public final class JsonObjectModel {
private final String[] parameterNames;
private final Method[] accessors;
private final Method[] defaultMethods;
private final Object defaultsReceiver;
private final int[] defaultMaskBits;
private final boolean[] parameterNullable;
private final TypeRef<?>[] parameterTypes;
Expand All @@ -68,13 +69,51 @@ public JsonObjectModel(
Method[] propertyGetters,
Method[] propertySetters,
TypeRef<?>[] propertyTypes) {
this(
constructor,
defaultConstructor,
parameterNames,
accessors,
defaultMethods,
null,
defaultMaskBits,
parameterNullable,
parameterTypes,
propertyNames,
propertyGetters,
propertySetters,
propertyTypes);
}

/**
* Creates one ordinary language object model whose constructor defaults are instance methods on
* {@code defaultsReceiver}. Scala emits {@code $lessinit$greater$default$N} on the companion
* singleton and mirrors it as a static forwarder on the case class only for a top-level
* companion, so a case class declared inside an {@code object} binds its defaults on that
* singleton. Pass {@code null} when the defaults are static members of the created type.
*/
public JsonObjectModel(
Constructor<?> constructor,
Constructor<?> defaultConstructor,
String[] parameterNames,
Method[] accessors,
Method[] defaultMethods,
Object defaultsReceiver,
int[] defaultMaskBits,
boolean[] parameterNullable,
TypeRef<?>[] parameterTypes,
String[] propertyNames,
Method[] propertyGetters,
Method[] propertySetters,
TypeRef<?>[] propertyTypes) {
this(
(Executable) constructor,
constructor,
defaultConstructor,
parameterNames,
accessors,
defaultMethods,
defaultsReceiver,
defaultMaskBits,
parameterNullable,
parameterTypes,
Expand Down Expand Up @@ -108,6 +147,7 @@ public JsonObjectModel(
parameterNames,
accessors,
defaultMethods,
null,
defaultMaskBits,
parameterNullable,
parameterTypes,
Expand Down Expand Up @@ -136,12 +176,49 @@ public JsonObjectModel(
TypeRef<?>[] propertyTypes,
boolean[] propertyReconstructible,
boolean[] propertyRequired) {
this(
creator,
invocationCreator,
defaultConstructor,
parameterNames,
accessors,
defaultMethods,
null,
defaultMaskBits,
parameterNullable,
parameterTypes,
propertyNames,
propertyGetters,
propertySetters,
propertyTypes,
propertyReconstructible,
propertyRequired);
}

private JsonObjectModel(
Executable creator,
Executable invocationCreator,
Constructor<?> defaultConstructor,
String[] parameterNames,
Method[] accessors,
Method[] defaultMethods,
Object defaultsReceiver,
int[] defaultMaskBits,
boolean[] parameterNullable,
TypeRef<?>[] parameterTypes,
String[] propertyNames,
Method[] propertyGetters,
Method[] propertySetters,
TypeRef<?>[] propertyTypes,
boolean[] propertyReconstructible,
boolean[] propertyRequired) {
this.creator = Objects.requireNonNull(creator, "creator");
this.invocationCreator = Objects.requireNonNull(invocationCreator, "invocationCreator");
this.defaultConstructor = defaultConstructor;
this.parameterNames = parameterNames.clone();
this.accessors = accessors.clone();
this.defaultMethods = defaultMethods.clone();
this.defaultsReceiver = defaultsReceiver;
this.defaultMaskBits = defaultMaskBits.clone();
this.parameterNullable = parameterNullable.clone();
this.parameterTypes = parameterTypes.clone();
Expand Down Expand Up @@ -169,6 +246,7 @@ private JsonObjectModel(
parameterNames = new String[0];
accessors = new Method[0];
defaultMethods = new Method[0];
defaultsReceiver = null;
defaultMaskBits = new int[0];
parameterNullable = new boolean[0];
parameterTypes = new TypeRef<?>[0];
Expand Down Expand Up @@ -294,6 +372,7 @@ private void validate() {
}
}
HashSet<String> names = new HashSet<>();
boolean hasDefaultMethod = false;
for (int i = 0; i < parameterNames.length; i++) {
String name = parameterNames[i];
if (name == null || name.isEmpty() || !names.add(name)) {
Expand All @@ -303,6 +382,23 @@ private void validate() {
if (defaultMethods[i] != null && defaultMaskBits[i] >= 0) {
throw new IllegalArgumentException("A constructor parameter has two default mechanisms");
}
if (defaultMethods[i] != null
&& Modifier.isStatic(defaultMethods[i].getModifiers()) == (defaultsReceiver != null)) {
throw new IllegalArgumentException(
"A JSON constructor default receiver is required exactly for instance defaults "
+ defaultMethods[i]);
}
if (defaultsReceiver != null
&& defaultMethods[i] != null
&& !defaultMethods[i].getDeclaringClass().isInstance(defaultsReceiver)) {
throw new IllegalArgumentException(
"JSON constructor default receiver does not own " + defaultMethods[i]);
}
hasDefaultMethod |= defaultMethods[i] != null;
}
if (defaultsReceiver != null && !hasDefaultMethod) {
throw new IllegalArgumentException(
"A JSON constructor default receiver requires at least one instance default");
}
names.clear();
for (int i = 0; i < propertyNames.length; i++) {
Expand Down Expand Up @@ -390,6 +486,11 @@ public Method[] defaultMethods() {
return defaultMethods.clone();
}

/** Returns the receiver of instance constructor-default methods, or null when they are static. */
public Object defaultsReceiver() {
return defaultsReceiver;
}

public int[] defaultMaskBits() {
return defaultMaskBits.clone();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,7 @@ private static JsonCreatorInfo buildObjectModelCreatorInfo(
creatorDefaults(rawTypes),
generatedCodec,
defaultMethods,
objectModel.defaultsReceiver(),
names,
objectModel.defaultConstructor(),
defaultMaskBits,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1134,9 +1134,10 @@ private boolean canCompileCreator(JsonCreatorInfo creator) {
return false;
}
Method defaultMethod = creator.defaultMethod(i);
// JsonCreatorInfo guarantees that a default method belongs to the creator owner and that its
// dependency types are the preceding creator parameters. The generated reader still invokes
// that exact method, so validate its access from the final definition context as well.
// JsonCreatorInfo guarantees that a default method belongs to the creator owner, or to the
// language singleton that owns instance defaults, and that its dependency types are the
// preceding creator parameters. The generated reader invokes that exact method on that exact
// declaring class, so validate its access from the final definition context as well.
if (defaultMethod != null && !canCall(defaultMethod)) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1307,14 +1307,36 @@ private Expression creatorDefaultValue(
inputs[i] = new Expression.Cast(arguments.values[i], TypeRef.of(dependencies[i])).inline();
}
Expression value =
new Expression.StaticInvoke(
method.getDeclaringClass(),
method.getName(),
TypeRef.of(method.getReturnType()),
inputs);
Modifier.isStatic(method.getModifiers())
? new Expression.StaticInvoke(
method.getDeclaringClass(),
method.getName(),
TypeRef.of(method.getReturnType()),
inputs)
: new Expression.Invoke(
defaultsReceiver(method),
method.getName(),
TypeRef.of(method.getReturnType()),
inputs);
return new Expression.Cast(value, TypeRef.of(parameterType));
}

/**
* Reads the language singleton that owns instance constructor defaults, such as a Scala
* companion. Each defaulted parameter needs its own expression, because generated code for one
* expression instance is emitted once at its first use site, and every use site here is a
* separate missing-argument block, so a shared instance would reference a local declared in a
* sibling block.
*/
private Expression defaultsReceiver(Method method) {
return new Expression.Cast(
new Expression.Invoke(
fieldRef("creator", JsonCreatorInfo.class),
"defaultsReceiver",
TypeRef.of(Object.class)),
TypeRef.of(method.getDeclaringClass()));
}

private Expression finishCreator(
JsonGeneratedCodecBuilder builder,
Class<?> type,
Expand Down Expand Up @@ -1485,13 +1507,19 @@ private void appendWorkspaceDefaults(
.append(";\n");
}
} else {
body.append("arguments[")
.append(i)
.append("] = ")
.append(ctx.type(method.getDeclaringClass()))
.append('.')
.append(method.getName())
.append('(');
body.append("arguments[").append(i).append("] = ");
if (Modifier.isStatic(method.getModifiers())) {
body.append(ctx.type(method.getDeclaringClass()));
} else {
// Fetched on the missing-argument branch, not once per construction: a creator whose
// properties are all present must not pay for a receiver it never reads.
body.append("((")
.append(ctx.type(method.getDeclaringClass()))
.append(") ")
.append(creatorExpression)
.append(".defaultsReceiver())");
}
body.append('.').append(method.getName()).append('(');
Class<?>[] dependencies = method.getParameterTypes();
for (int j = 0; j < dependencies.length; j++) {
if (j != 0) {
Expand Down
Loading
Loading