starlark: relax Param.noneable to ease its removal
The semantics of noneable are now additive, not an override:
a client may specify allowedTypes={..., NoneType} and not mention
noneable, and the parameter will accept None.
Follow-up changes will remove all uses of noneable
in copybara (CL 336078992) and Bazel.
PiperOrigin-RevId: 336117855
diff --git a/src/main/java/net/starlark/java/annot/Param.java b/src/main/java/net/starlark/java/annot/Param.java
index 7cad3c5..210477c 100644
--- a/src/main/java/net/starlark/java/annot/Param.java
+++ b/src/main/java/net/starlark/java/annot/Param.java
@@ -51,12 +51,17 @@
* #allowedTypes}. Specifying neither {@code type} nor {@code allowedTypes} is equivalent to
* specifying the class of the parameter variable.
*/
- // TODO(adonovan): abolish this (and noneable). It creates ambiguity, complexity,and bugs.
- // Just specify allowedTypes, with empty array meaning "use the class of the parameter itself".
+ // Deprecated. Use allowedTypes.
Class<?> type() default Void.class;
/**
- * List of allowed types for the parameter if multiple types are allowed.
+ * List of allowed types for the parameter.
+ *
+ * <p>The array may be omitted, in which case the parameter accepts any value whose class is
+ * assignable to the class of the parameter variable.
+ *
+ * <p>If a function should accept None, NoneType should be in this list. (Currently one may set
+ * {@link #noneable} to achieve the same effect, but it is going away.)
*
* <p>May not be used in conjunction with {@link #type}.
*/
@@ -71,24 +76,18 @@
* runtime, so the Java method signature should use a generic type of Object and cast
* appropriately.
*/
+ // Deprecated. Use allowedTypes.
Class<?> generic1() default Object.class;
/**
- * Indicates whether this parameter accepts {@code None} as a value, its allowed types
- * notwithstanding.
+ * Indicates whether this parameter accepts {@code None} as a value, even if NoneType was not
+ * among {@link #allowedTypes}.
*
* <p>If true, {@code None} is accepted as a valid input in addition to the types mentioned by
* {@link #type} or {@link #allowedTypes}. In this case, the Java type of the corresponding method
* parameter must be {@code Object}.
- *
- * <p>If false, this parameter cannot be passed {@code None}, even if it would otherwise be
- * allowed by {@code type} or {@code allowedTypes}.
*/
- // TODO(starlark-team): Allow None as a value when noneable is false and the type is Object. But
- // look out for unwanted user-visible changes in the signatures of builtins.
- // TODO(140932420): Consider simplifying noneable by converting None to null, so that the Java
- // type need not be Object. But note that we still have the same problem for params whose default
- // value is the special "unbound" sentinel.
+ // Deprecated. Use allowedTypes={..., @ParamType(type=NoneType)}.
boolean noneable() default false;
/**
@@ -160,8 +159,4 @@
* {@link #defaultValue}.)
*/
String valueWhenDisabled() default "";
-
- // TODO(bazel-team): parse the type from a single field in Starlark syntax,
- // and allow a Union as "ThisType or ThatType or NoneType":
- // String type() default "Object";
}
diff --git a/src/main/java/net/starlark/java/annot/ParamType.java b/src/main/java/net/starlark/java/annot/ParamType.java
index 6ac3bb1..de1f8ca 100644
--- a/src/main/java/net/starlark/java/annot/ParamType.java
+++ b/src/main/java/net/starlark/java/annot/ParamType.java
@@ -34,5 +34,7 @@
* runtime, so the Java method signature should use a generic type of Object and cast
* appropriately.
*/
+ // TODO(adonovan): make this an array---a non-breaking change for most clients---
+ // ideally of the same length as the number of type parameters of type().
Class<?> generic1() default Object.class;
}
diff --git a/src/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java b/src/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java
index 9103eb0..7227937 100644
--- a/src/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java
+++ b/src/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java
@@ -309,26 +309,6 @@
Element param, Param paramAnnot, TypeMirror objectType, TypeMirror voidType) {
TypeMirror paramType = param.asType(); // type of the Java method parameter
- // A "noneable" parameter variable must accept the value None.
- // A parameter whose default is None must be noneable.
- if (paramAnnot.noneable()) {
- if (!types.isSameType(paramType, objectType)) {
- errorf(
- param,
- "Expected type 'Object' but got type '%s' for noneable parameter '%s'. The argument"
- + " for a noneable parameter may be None, so the java parameter must be"
- + " compatible with the type of None as well as possible non-None values.",
- paramType,
- param.getSimpleName());
- }
- } else if (paramAnnot.defaultValue().equals("None")) {
- errorf(
- param,
- "Parameter '%s' has 'None' default value but is not noneable. (If this is intended"
- + " as a mandatory parameter, leave the defaultValue field empty)",
- paramAnnot.name());
- }
-
// Give helpful hint for parameter of type Integer.
TypeMirror integerType = getType("java.lang.Integer");
if (types.isSameType(paramType, integerType)) {
@@ -361,17 +341,24 @@
}
}
- // Reject an Param.allowed_type if not assignable to parameter variable.
+ TypeMirror noneType = getType("net.starlark.java.eval.NoneType");
+ boolean allowsNoneType = false;
+
+ // Reject an entry of Param.allowedTypes if not assignable to the parameter variable.
for (ParamType paramTypeAnnot : paramAnnot.allowedTypes()) {
TypeMirror t = getParamTypeType(paramTypeAnnot);
if (!types.isAssignable(t, types.erasure(paramType))) {
errorf(
param,
- "annotated allowed_type %s of parameter '%s' is not assignable to variable of type %s",
+ "annotated allowedTypes entry %s of parameter '%s' is not assignable to variable of "
+ + "type %s",
t,
paramAnnot.name(),
paramType);
}
+ if (types.isSameType(t, noneType)) {
+ allowsNoneType = true;
+ }
}
// Reject generic types C<T> other than C<?>,
@@ -391,6 +378,26 @@
}
}
+ // A "noneable" parameter variable must accept the value None.
+ // A parameter whose default is None must be noneable.
+ if (paramAnnot.noneable()) {
+ if (!types.isSameType(paramType, objectType)) {
+ errorf(
+ param,
+ "Expected type 'Object' but got type '%s' for noneable parameter '%s'. The argument"
+ + " for a noneable parameter may be None, so the java parameter must be"
+ + " compatible with the type of None as well as possible non-None values.",
+ paramType,
+ param.getSimpleName());
+ }
+ } else if (paramAnnot.defaultValue().equals("None") && !allowsNoneType) {
+ errorf(
+ param,
+ "Parameter '%s' has 'None' default value but is not noneable. (If this is intended"
+ + " as a mandatory parameter, leave the defaultValue field empty)",
+ paramAnnot.name());
+ }
+
// Check sense of flag-controlled parameters.
boolean hasFlag = false;
if (!paramAnnot.enableOnlyWithFlag().isEmpty()) {
diff --git a/src/main/java/net/starlark/java/eval/BuiltinCallable.java b/src/main/java/net/starlark/java/eval/BuiltinCallable.java
index a3f1342..5ed34ae 100644
--- a/src/main/java/net/starlark/java/eval/BuiltinCallable.java
+++ b/src/main/java/net/starlark/java/eval/BuiltinCallable.java
@@ -350,19 +350,6 @@
"in call to %s(), parameter '%s' got value of type '%s', want '%s'",
methodName, param.getName(), Starlark.type(value), param.getTypeErrorMessage());
}
-
- // None is valid if and only if the parameter is marked noneable,
- // in which case the above check passes as the list of classes will include NoneType.
- // The reason for this check is to ensure that merely having type=Object.class
- // does not allow None as an argument value; I'm not sure why, that but that's the
- // historical behavior.
- //
- // We do this check second because the first check prints a better error
- // that enumerates the allowed types.
- if (value == Starlark.NONE && !param.isNoneable()) {
- throw Starlark.errorf(
- "in call to %s(), parameter '%s' cannot be None", methodName, param.getName());
- }
}
// Returns a phrase meaning "disabled" appropriate to the specified flag.
diff --git a/src/main/java/net/starlark/java/eval/ParamDescriptor.java b/src/main/java/net/starlark/java/eval/ParamDescriptor.java
index 811e505..f62b221 100644
--- a/src/main/java/net/starlark/java/eval/ParamDescriptor.java
+++ b/src/main/java/net/starlark/java/eval/ParamDescriptor.java
@@ -31,7 +31,6 @@
private final String name;
@Nullable private final Object defaultValue;
- private final boolean noneable;
private final boolean named;
private final boolean positional;
private final List<Class<?>> allowedClasses; // non-empty
@@ -42,14 +41,14 @@
private ParamDescriptor(
String name,
String defaultExpr,
- boolean noneable,
boolean named,
boolean positional,
List<Class<?>> allowedClasses,
@Nullable String disabledByFlag) {
this.name = name;
+ // TODO(adonovan): apply the same validation logic to the default value
+ // as we do to caller-supplied values (see BuiltinCallable.checkParamValue).
this.defaultValue = defaultExpr.isEmpty() ? null : evalDefault(name, defaultExpr);
- this.noneable = noneable;
this.named = named;
this.positional = positional;
this.allowedClasses = allowedClasses;
@@ -87,17 +86,14 @@
} else {
allowedClasses.add(param.type());
}
- if (param.noneable()) {
- // A few annotations redundantly declare NoneType.
- if (!allowedClasses.contains(NoneType.class)) {
- allowedClasses.add(NoneType.class);
- }
+
+ if (param.noneable() && !allowedClasses.contains(NoneType.class)) {
+ allowedClasses.add(NoneType.class);
}
return new ParamDescriptor(
param.name(),
defaultExpr,
- param.noneable(),
param.named(),
param.positional(),
allowedClasses,
@@ -118,11 +114,6 @@
return allowedClasses;
}
- /** @see Param#noneable() */
- boolean isNoneable() {
- return noneable;
- }
-
/** @see Param#positional() */
boolean isPositional() {
return positional;
diff --git a/src/test/java/net/starlark/java/eval/StarlarkEvaluationTest.java b/src/test/java/net/starlark/java/eval/StarlarkEvaluationTest.java
index 4f3a9d3a..b5f5ccf 100644
--- a/src/test/java/net/starlark/java/eval/StarlarkEvaluationTest.java
+++ b/src/test/java/net/starlark/java/eval/StarlarkEvaluationTest.java
@@ -237,7 +237,7 @@
positional = false,
named = true),
@Param(
- name = "nonNoneable",
+ name = "acceptsAny",
type = Object.class,
defaultValue = "\"a\"",
positional = false,
@@ -267,7 +267,7 @@
boolean posOrNamed,
boolean named,
boolean optionalNamed,
- Object nonNoneable,
+ Object acceptsAny,
Object noneable,
Object multi) {
return "with_params("
@@ -281,7 +281,7 @@
+ ", "
+ optionalNamed
+ ", "
- + nonNoneable
+ + acceptsAny
+ (noneable != Starlark.NONE ? ", " + noneable : "")
+ (multi != Starlark.NONE ? ", " + multi : "")
+ ")";
@@ -312,7 +312,7 @@
positional = false,
named = true),
@Param(
- name = "nonNoneable",
+ name = "acceptsAny",
type = Object.class,
defaultValue = "\"a\"",
positional = false,
@@ -343,7 +343,7 @@
boolean posOrNamed,
boolean named,
boolean optionalNamed,
- Object nonNoneable,
+ Object acceptsAny,
Object noneable,
Object multi,
StarlarkThread thread) {
@@ -358,7 +358,7 @@
+ ", "
+ optionalNamed
+ ", "
- + nonNoneable
+ + acceptsAny
+ (noneable != Starlark.NONE ? ", " + noneable : "")
+ (multi != Starlark.NONE ? ", " + multi : "")
+ ", "
@@ -1166,9 +1166,9 @@
ev.new Scenario()
.update("mock", new Mock())
.setUp("")
- .testIfExactError(
- "in call to with_params(), parameter 'nonNoneable' cannot be None",
- "mock.with_params(1, True, True, named=True, optionalNamed=False, nonNoneable=None)");
+ .testExpression(
+ "mock.with_params(1, True, True, named=True, optionalNamed=False, acceptsAny=None)",
+ "with_params(1, true, true, true, false, None)");
ev.new Scenario()
.update("mock", new Mock())
diff --git a/src/test/java/net/starlark/java/eval/testdata/int_constructor.sky b/src/test/java/net/starlark/java/eval/testdata/int_constructor.sky
index 840b430..2ca2584 100644
--- a/src/test/java/net/starlark/java/eval/testdata/int_constructor.sky
+++ b/src/test/java/net/starlark/java/eval/testdata/int_constructor.sky
@@ -14,7 +14,7 @@
assert_eq(int(False), 0)
# from other
-int(None) ### parameter 'x' cannot be None
+int(None) ### got NoneType, want string, int, or bool
---
# from string