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 d6bdad3bc fix(compiler): reject optional any in IDL validation (#3807)
d6bdad3bc is described below
commit d6bdad3bc55c3c60faa91d41f85d06d16cbf4d86
Author: Peiyang He <[email protected]>
AuthorDate: Wed Jul 1 13:33:13 2026 +0800
fix(compiler): reject optional any in IDL validation (#3807)
## Why?
Reject `optional any` and `any [nullable = true]` in IDL to prevent
compilation errors in C++ and Rust.
## What does this PR do?
1. Reject `optional any` and `any [nullable = true]` in `validator.py`.
2. Change testcases and doc accordingly.
## Related issues
Closes https://github.com/apache/fory/issues/3805
## AI Contribution Checklist
- [X] Substantial AI assistance was used in this PR: `no`
## Does this PR introduce any user-facing change?
Yes, users now cannot use `optional any` and `any [nullable = true]` in
IDL.
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A.
---
compiler/fory_compiler/ir/validator.py | 36 +++++++++++
.../fory_compiler/tests/test_xlang_type_system.py | 71 ++++++++++++++++++----
docs/compiler/schema-idl.md | 16 ++++-
3 files changed, 109 insertions(+), 14 deletions(-)
diff --git a/compiler/fory_compiler/ir/validator.py
b/compiler/fory_compiler/ir/validator.py
index fa2455f39..7c99c6a27 100644
--- a/compiler/fory_compiler/ir/validator.py
+++ b/compiler/fory_compiler/ir/validator.py
@@ -47,6 +47,7 @@ INVALID_MAP_KEY_KINDS = {
PrimitiveKind.DECIMAL,
}
INVALID_MAP_KEY_MESSAGE = "map keys do not support any, binary, float,
decimal, message, union, list, map, or array types"
+OPTIONAL_ANY_MESSAGE = "optional or nullable any is not supported; use any
instead"
@dataclass
@@ -81,6 +82,7 @@ class SchemaValidator:
self._check_type_references()
self._check_services()
self._check_collection_type_rules()
+ self._check_optional_any_rules()
if not self.allow_nested_collections:
self._check_collection_nesting()
self._check_ref_rules()
@@ -628,6 +630,40 @@ class SchemaValidator:
for f in union.fields:
check_type(f.field_type, f)
+ def _check_optional_any_rules(self) -> None:
+ def is_any_type(field_type: FieldType) -> bool:
+ return (
+ isinstance(field_type, PrimitiveType)
+ and field_type.kind == PrimitiveKind.ANY
+ )
+
+ def check_field(field: Field) -> None:
+ field_type = field.field_type
+ if field.optional and is_any_type(field_type):
+ self._error(OPTIONAL_ANY_MESSAGE, field.location)
+
+ if isinstance(field_type, ListType):
+ if field_type.element_optional and
is_any_type(field_type.element_type):
+ self._error(OPTIONAL_ANY_MESSAGE, field.location)
+ elif isinstance(field_type, MapType):
+ if field_type.value_optional and
is_any_type(field_type.value_type):
+ self._error(OPTIONAL_ANY_MESSAGE, field.location)
+
+ def check_message_fields(message: Message) -> None:
+ for f in message.fields:
+ check_field(f)
+ for nested_msg in message.nested_messages:
+ check_message_fields(nested_msg)
+ for nested_union in message.nested_unions:
+ for f in nested_union.fields:
+ check_field(f)
+
+ for message in self.schema.messages:
+ check_message_fields(message)
+ for union in self.schema.unions:
+ for f in union.fields:
+ check_field(f)
+
def _check_collection_nesting(self) -> None:
def check_field(
field: Field, enclosing_messages: Optional[List[Message]] = None
diff --git a/compiler/fory_compiler/tests/test_xlang_type_system.py
b/compiler/fory_compiler/tests/test_xlang_type_system.py
index 4924dc60c..9ee546d6f 100644
--- a/compiler/fory_compiler/tests/test_xlang_type_system.py
+++ b/compiler/fory_compiler/tests/test_xlang_type_system.py
@@ -24,7 +24,11 @@ from fory_compiler.frontend.proto import ProtoFrontend
from fory_compiler.ir.ast import ArrayType, ListType, MapType, PrimitiveType
from fory_compiler.ir.emitter import FDLEmitter
from fory_compiler.ir.types import PrimitiveKind
-from fory_compiler.ir.validator import SchemaValidator
+from fory_compiler.ir.validator import (
+ INVALID_MAP_KEY_MESSAGE,
+ OPTIONAL_ANY_MESSAGE,
+ SchemaValidator,
+)
def parse_schema(source: str):
@@ -188,11 +192,7 @@ def test_map_rejects_non_portable_key_types(key_type):
)
assert not ok
- assert any(
- "map keys do not support any, binary, float, decimal, message, union,
list, map, or array types"
- in err.message
- for err in validator.errors
- )
+ assert any(INVALID_MAP_KEY_MESSAGE in err.message for err in
validator.errors)
@pytest.mark.parametrize(
@@ -271,11 +271,7 @@ def test_map_rejects_message_and_union_key_types(source):
_schema, validator, ok = validate_schema(source)
assert not ok
- assert any(
- "map keys do not support any, binary, float, decimal, message, union,
list, map, or array types"
- in err.message
- for err in validator.errors
- )
+ assert any(INVALID_MAP_KEY_MESSAGE in err.message for err in
validator.errors)
@pytest.mark.parametrize(
@@ -321,6 +317,59 @@ def test_map_accepts_enum_key_types(source):
assert ok, validator.errors
[email protected](
+ "source",
+ [
+ """
+ message Invalid {
+ optional any value = 1;
+ }
+ """,
+ """
+ message Invalid {
+ any value = 1 [nullable = true];
+ }
+ """,
+ """
+ message Invalid {
+ list<optional any> values = 1;
+ }
+ """,
+ """
+ message Invalid {
+ map<string, optional any> values = 1;
+ }
+ """,
+ ],
+)
+def test_optional_any_is_rejected(source):
+ _schema, validator, ok = validate_schema(source)
+
+ assert not ok
+ assert any(OPTIONAL_ANY_MESSAGE in err.message for err in validator.errors)
+
+
[email protected](
+ "source",
+ [
+ """
+ message Valid {
+ any value = 1;
+ }
+ """,
+ """
+ message Valid {
+ optional list<any> values = 1;
+ }
+ """,
+ ],
+)
+def test_non_direct_optional_any_is_accepted(source):
+ _schema, validator, ok = validate_schema(source)
+
+ assert ok, validator.errors
+
+
def test_proto_repeated_fields_remain_list_type():
schema = ProtoFrontend().parse(
"""
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index 128f2fe2a..d26083e5f 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -993,9 +993,12 @@ list_type := 'list' '<' { 'optional' | 'ref' |
scalar_encoding } field_type '
array_type := 'array' '<' array_element_type '>'
```
-`optional` before `list` applies to the collection field. `ref` is only valid
-for named message/union fields; for collection contents, use `list<ref T>` or
-`map<K, ref V>`. `repeated` is accepted as an alias for `list`.
+`optional` before `list` applies to the collection field. `optional` is not
+supported when it is applied directly to `any`; use `any`, `list<any>`, or
+`map<K, any>` instead of `optional any`, `list<optional any>`, or
+`map<K, optional any>`. `ref` is only valid for named message/union fields; for
+collection contents, use `list<ref T>` or `map<K, ref V>`. `repeated` is
+accepted as an alias for `list`.
### Field Modifiers
@@ -1010,6 +1013,10 @@ message User {
}
```
+Do not use `optional` or `[nullable = true]` directly on `any`. The compiler
+rejects `optional any`, `any [nullable = true]`, `list<optional any>`, and
+`map<K, optional any>`; use `any`, `list<any>`, or `map<K, any>` instead.
+
**Generated Code:**
| Language | Non-optional | Optional
|
@@ -1389,6 +1396,9 @@ message Envelope [id=122] {
**Notes:**
- `any` always writes a null flag (same as `nullable`) because values may be
empty.
+- `optional` and `[nullable = true]` are not supported directly on `any`; use
+ `any`, `list<any>`, or `map<K, any>` instead of `optional any`,
+ `list<optional any>`, or `map<K, optional any>`.
- Allowed dynamic values are limited to `bool`, `string`, `enum`, `message`,
and `union`.
Other primitives (numeric, bytes, date/time) and list/map are not supported;
wrap them in a
message or use explicit fields instead.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]