You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/07/28 03:07:08 UTC
(fory) branch main updated: feat(dart): add dart inheriance support (#3892)
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 c91f7c562 feat(dart): add dart inheriance support (#3892)
c91f7c562 is described below
commit c91f7c562b6f4b92cec73df60b04f3905f48ae37
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Tue Jul 28 11:07:03 2026 +0800
feat(dart): add dart inheriance support (#3892)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] 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 or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, 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
---
.agents/languages/dart.md | 37 +
.github/workflows/ci.yml | 39 +-
README.md | 2 +-
benchmarks/dart/run.sh | 2 +-
dart/README.md | 13 +-
.../fory-test/lib/entity/xlang_test_manual.dart | 2 +-
.../fory-test/lib/model/inheritance_models.dart | 235 +++
dart/packages/fory-test/pubspec.yaml | 3 +-
.../test/cross_lang_test/xlang_test_main.dart | 44 +
.../fory-test/test/struct_inheritance_test.dart | 338 ++++
.../packages/fory-test/tool/inheritance_smoke.dart | 46 +
dart/packages/fory/CHANGELOG.md | 12 +
dart/packages/fory/README.md | 95 +-
.../fory/lib/src/annotation/fory_field.dart | 21 +
.../fory/lib/src/annotation/fory_struct.dart | 18 +-
.../lib/src/codegen/fory_constructor_analysis.dart | 874 +++++++++
.../fory/lib/src/codegen/fory_generator.dart | 2058 +++++++++++++++++---
.../fory/lib/src/codegen/generated_support.dart | 2 -
dart/packages/fory/lib/src/meta/field_info.dart | 12 +
.../fory/lib/src/resolver/type_resolver.dart | 63 +-
.../fory/lib/src/serializer/scalar_conversion.dart | 35 +-
.../fory/lib/src/serializer/struct_serializer.dart | 39 +-
dart/packages/fory/pubspec.yaml | 4 +-
dart/packages/fory/test/codegen_test_support.dart | 132 ++
.../fory/test/external_type_generator_test.dart | 70 +-
.../fory/test/graph_memory_budget_test.dart | 163 ++
.../fory/test/runtime_validation_test.dart | 30 -
.../struct_constructor_flow_generator_test.dart | 544 ++++++
.../test/struct_inheritance_generator_test.dart | 625 ++++++
.../struct_private_companion_generator_test.dart | 1005 ++++++++++
dart/packages/fory/test/xlang_protocol_test.dart | 278 +++
.../analysis_options.yaml} | 22 +-
.../inheritance-test-models/lib/base_models.dart | 92 +
.../inheritance-test-models/lib/middle_models.dart | 52 +
.../pubspec.yaml | 15 +-
dart/pubspec.yaml | 3 +-
docs/guide/dart/code-generation.md | 93 +-
docs/guide/dart/external-types.md | 11 +-
docs/guide/dart/grpc-support.md | 4 +-
docs/guide/dart/index.md | 10 +-
docs/guide/dart/schema-evolution.md | 23 +
docs/guide/dart/schema-metadata.md | 21 +-
docs/guide/dart/supported-types.md | 18 +-
docs/guide/dart/troubleshooting.md | 56 +-
docs/guide/dart/type-registration.md | 12 +
docs/guide/dart/web-platform-support.md | 25 +-
docs/guide/dart/xlang-serialization.md | 17 +-
docs/specification/xlang_implementation_guide.md | 187 +-
integration_tests/idl_tests/run_dart_tests.sh | 2 +-
.../java/org/apache/fory/xlang/DartXlangTest.java | 89 +-
50 files changed, 7121 insertions(+), 472 deletions(-)
diff --git a/.agents/languages/dart.md b/.agents/languages/dart.md
index 814b64aad..47613198b 100644
--- a/.agents/languages/dart.md
+++ b/.agents/languages/dart.md
@@ -19,6 +19,43 @@ Load this file when changing `dart/`.
existing resolver, field, carrier, reference, and compatible-read paths. Do
not add runtime declaration instances, callbacks, member lookup, target-copy
objects, a second registration API, or a second carrier/root flow.
+- Ordinary `ForyStruct` generation must discover every real instance storage
+ field on the concrete superclass and applied-mixin chain before considering
+ Dart accessibility. Enumerate each instantiated hierarchy layer's declared
+ fields; do not use only the child `element.fields`, `allSupertypes`, member
+ lookup, import visibility, or privacy as field discovery.
+- Process each discovered ordinary field through one owner path: apply only
+ its declaration-owned `ForyField(ignore: true)`, substitute the concrete
+ generic type, resolve direct or companion access, prove construction, then
+ build the concrete child's one globally sorted flattened schema. Never omit
+ a non-ignored field because it is private, inaccessible, final, hidden, or
+ unsupported; report a generation error.
+- Public inherited fields and private inherited fields declared in the child's
+ library use direct generated access without a parent annotation.
+ Cross-library private storage requires
+ `ForyStruct(exposePrivateFields: true)` on a public hierarchy boundary in
+ that field's declaring library. That provider emits only public typed static
+ getter/setter access for its own library's private state; it owns no schema,
+ serializer, construction, registration, reference, or graph-memory state.
+ Each declaring library in a multi-library hierarchy must provide its own
+ boundary. A consumer annotation cannot authorize another library's field.
+- A non-ignored ordinary `final` or `late final` field must receive its decoded
+ value unchanged from the selected concrete-child constructor through
+ element-proven field formals, super formals, redirects, or explicit
+ initializer/super arguments. Names are not bindings. Do not post-set final
+ state, use reflection, or fall back when identity flow cannot be proven.
+- The concrete ordinary child owns one flattened schema, serializer,
+ constructor reconstruction, reference publication path, and graph-memory
+ object charge. Inherited reference metadata enters the existing recursive
+ reference analysis once. Do not add reference state, change `ReadContext` or
+ serializer reference-call contracts, or invoke a parent serializer for
+ inheritance.
+- Keep external target fields explicit. Ordinary hierarchy discovery and
+ `exposePrivateFields` must never scan or expose an external target hierarchy.
+- Across packages, generate and publish the provider library's `.fory.dart`
+ part before building a consumer that uses its private-field access companion.
+ A direct import or re-export must expose both the public boundary and
+ companion.
- Constructor-based external structural serializers must reject every
statically provable reference-tracked path back to the target, including
paths through supported list, set, and map type arguments. Do not simulate
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 52bee60ef..450bd1a73 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1742,11 +1742,15 @@ jobs:
run: |
(
cd dart/packages/fory
- dart run build_runner build --delete-conflicting-outputs
+ dart run build_runner build
+ )
+ (
+ cd dart/packages/inheritance-test-models
+ dart run build_runner build
)
(
cd dart/packages/fory-test
- dart run build_runner build --delete-conflicting-outputs
+ dart run build_runner build
)
- name: Run Dart Xlang Test
env:
@@ -1781,16 +1785,37 @@ jobs:
run: |
(
cd dart/packages/fory
- dart run build_runner build --delete-conflicting-outputs
+ dart run build_runner build
+ )
+ (
+ cd dart/packages/inheritance-test-models
+ dart run build_runner build
+ )
+ (
+ cd dart/packages/fory-test
+ dart run build_runner build
+ )
+ - name: Run VM tests
+ run: |
+ (
+ cd dart/packages/fory
+ dart test
)
(
cd dart/packages/fory-test
- dart run build_runner build --delete-conflicting-outputs
+ dart test
)
- - name: Run tests
+ - name: Run inheritance AOT and JavaScript smoke
run: |
cd dart/packages/fory-test
- dart test
+ dart compile exe \
+ -o "$RUNNER_TEMP/fory_inheritance_smoke" \
+ tool/inheritance_smoke.dart
+ "$RUNNER_TEMP/fory_inheritance_smoke"
+ dart compile js --fatal-warnings --no-source-maps -O2 \
+ -o "$RUNNER_TEMP/fory_inheritance_smoke.js" \
+ tool/inheritance_smoke.dart
+ node "$RUNNER_TEMP/fory_inheritance_smoke.js"
- name: Run web tests
run: |
cd dart/packages/fory
@@ -1910,7 +1935,7 @@ jobs:
- name: Generate Flutter smoke code
run: |
cd "$FLUTTER_SMOKE_DIR"
- dart run build_runner build --delete-conflicting-outputs
+ dart run build_runner build
- name: Run Flutter smoke tests
run: |
cd "$FLUTTER_SMOKE_DIR"
diff --git a/README.md b/README.md
index e358a480c..e124a3463 100644
--- a/README.md
+++ b/README.md
@@ -561,7 +561,7 @@ Dart uses the xlang wire format directly. Generate the companion file before
running:
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
**Swift**
diff --git a/benchmarks/dart/run.sh b/benchmarks/dart/run.sh
index 5813adac2..a1869a7f2 100644
--- a/benchmarks/dart/run.sh
+++ b/benchmarks/dart/run.sh
@@ -107,7 +107,7 @@ echo "Preparing Dart benchmark package..."
echo "============================================"
dart pub get
./tool/generate_proto.sh
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
echo ""
echo "============================================"
diff --git a/dart/README.md b/dart/README.md
index 54dc174c6..531df21b5 100644
--- a/dart/README.md
+++ b/dart/README.md
@@ -18,6 +18,7 @@ including getting started, API reference, and code examples.
| `packages/fory/example/` | Annotated example with generated output |
| `packages/fory/test/` | Unit and integration tests |
| `packages/external-type-test-models/` | Fory-free dependency models for external-type tests |
+| `packages/inheritance-test-models/` | Cross-library inheritance test providers |
| `test/` | Cross-language integration tests |
## Type Mapping
@@ -81,7 +82,7 @@ class Person {
```
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
Serialize and deserialize:
@@ -94,6 +95,14 @@ final bytes = fory.serialize(Person()..name = 'Ada'..age = 36);
final roundTrip = fory.deserialize<Person>(bytes);
```
+Ordinary annotated classes serialize one flattened view of their concrete
+superclass and applied-mixin storage. Public and same-library private inherited
+fields need no parent annotation. A declaring library that intentionally
+exposes private state to generated serializers in other libraries marks a
+public hierarchy boundary with `@ForyStruct(exposePrivateFields: true)`.
+`@ForyField(ignore: true)` on the field declaration is the only way to omit
+ordinary storage.
+
## Development
Run tests from the workspace root:
@@ -107,5 +116,5 @@ Run the code generator on the example:
```bash
cd packages/fory
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
diff --git a/dart/packages/fory-test/lib/entity/xlang_test_manual.dart b/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
index 05ea5f6e8..7f4e4c66b 100644
--- a/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
+++ b/dart/packages/fory-test/lib/entity/xlang_test_manual.dart
@@ -103,7 +103,7 @@ const GeneratedFieldType _refOverrideElementFieldType = GeneratedFieldType(
const List<GeneratedFieldInfo>
_refOverrideContainerForyFieldInfo = <GeneratedFieldInfo>[
// Keep this manual fixture in the same protocol order as generated structs:
- // all fields are non-primitive, so field identifiers order the wire payload.
+ // all fields are non-primitive, so wire names order the payload.
GeneratedFieldInfo(
name: 'listField',
identifier: 'list_field',
diff --git a/dart/packages/fory-test/lib/model/inheritance_models.dart b/dart/packages/fory-test/lib/model/inheritance_models.dart
new file mode 100644
index 000000000..716dc8b71
--- /dev/null
+++ b/dart/packages/fory-test/lib/model/inheritance_models.dart
@@ -0,0 +1,235 @@
+/*
+ * 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.
+ */
+
+library;
+
+import 'package:fory/fory.dart';
+import 'package:inheritance_test_models/base_models.dart' as base;
+import 'package:inheritance_test_models/middle_models.dart' as middle;
+
+part 'inheritance_models.fory.dart';
+
+@ForyStruct()
+final class CrossLibraryChild extends middle.PublicMiddleBoundary {
+ // Keep explicit forwarding to exercise constructor identity analysis.
+ // ignore: use_super_parameters
+ CrossLibraryChild(String rootValue, int middleValue, this.childFinal)
+ : super(rootValue, middleValue);
+
+ @ForyField(id: 21)
+ final bool childFinal;
+
+ @ForyField(id: 22)
+ String childMutable = '';
+}
+
+class OptionalMutableBase {
+ OptionalMutableBase([int decoded = 0])
+ : inheritedMutable = decoded,
+ observedAtConstruction = decoded;
+
+ @ForyField(id: 1)
+ int inheritedMutable;
+
+ @ForyField(ignore: true)
+ final int observedAtConstruction;
+}
+
+@ForyStruct()
+final class OptionalMutableChild extends OptionalMutableBase {
+ OptionalMutableChild(this.fixed, [int decoded = 0]) : super(decoded);
+
+ @ForyField(id: 2)
+ final int fixed;
+}
+
+class AllWritableOptionalBase {
+ AllWritableOptionalBase([int decoded = 0])
+ : inheritedMutable = decoded,
+ observedAtConstruction = decoded;
+
+ @ForyField(id: 1)
+ int inheritedMutable;
+
+ @ForyField(ignore: true)
+ final int observedAtConstruction;
+}
+
+@ForyStruct()
+final class AllWritableOptionalChild extends AllWritableOptionalBase {
+ AllWritableOptionalChild([super.decoded]);
+}
+
+@ForyStruct()
+final class XlangInheritedChild extends base.XlangInheritanceBoundary {
+ XlangInheritedChild();
+
+ @ForyField(id: 1, type: Int32Type())
+ int childInt = 0;
+
+ @ForyField(id: 5)
+ bool childFlag = false;
+
+ @ForyField(id: 9)
+ String childText = '';
+}
+
+class PublicXlangInheritanceParent {
+ @ForyField(id: 2, type: Int32Type())
+ int parentInt = 0;
+
+ @ForyField(id: 6)
+ bool parentFlag = false;
+
+ @ForyField(id: 10)
+ String parentText = '';
+}
+
+@ForyStruct()
+final class PublicXlangInheritedChild extends PublicXlangInheritanceParent {
+ PublicXlangInheritedChild();
+
+ @ForyField(id: 1, type: Int32Type())
+ int childInt = 0;
+
+ @ForyField(id: 5)
+ bool childFlag = false;
+
+ @ForyField(id: 9)
+ String childText = '';
+}
+
+@ForyStruct()
+final class FlatXlangInheritedStruct {
+ FlatXlangInheritedStruct();
+
+ @ForyField(id: 1, type: Int32Type())
+ int childInt = 0;
+
+ @ForyField(id: 2, type: Int32Type())
+ int parentInt = 0;
+
+ @ForyField(id: 5)
+ bool childFlag = false;
+
+ @ForyField(id: 6)
+ bool parentFlag = false;
+
+ @ForyField(id: 9)
+ String childText = '';
+
+ @ForyField(id: 10)
+ String parentText = '';
+}
+
+@ForyStruct()
+class CompatibleOldParent {
+ CompatibleOldParent();
+
+ @ForyField(id: 1)
+ String inheritedStable = '';
+}
+
+@ForyStruct()
+final class CompatibleOldChild extends CompatibleOldParent {
+ CompatibleOldChild();
+
+ @ForyField(id: 3)
+ String childStable = '';
+}
+
+@ForyStruct()
+class CompatibleNewParent {
+ CompatibleNewParent();
+
+ @ForyField(id: 1)
+ String inheritedStable = '';
+
+ @ForyField(id: 2)
+ String inheritedAdded = 'new-default';
+}
+
+@ForyStruct()
+final class CompatibleNewChild extends CompatibleNewParent {
+ CompatibleNewChild();
+
+ @ForyField(id: 3)
+ String childStable = '';
+}
+
+@ForyStruct()
+final class ReferenceLeaf {
+ ReferenceLeaf();
+
+ @ForyField(id: 1)
+ String label = '';
+}
+
+class ReferenceFieldsBase<T> {
+ ReferenceFieldsBase();
+
+ @ForyField(id: 1, ref: true)
+ T? first;
+
+ @ForyField(id: 2, ref: true)
+ T? second;
+
+ @ListField(id: 3, element: DeclaredType(ref: true))
+ List<T> nested = <T>[];
+}
+
+@ForyStruct()
+final class InheritedReferenceModel extends ReferenceFieldsBase<ReferenceLeaf> {
+ InheritedReferenceModel();
+}
+
+@ForyStruct()
+final class FlatReferenceModel {
+ FlatReferenceModel();
+
+ @ForyField(id: 1, ref: true)
+ ReferenceLeaf? first;
+
+ @ForyField(id: 2, ref: true)
+ ReferenceLeaf? second;
+
+ @ListField(id: 3, element: DeclaredType(ref: true))
+ List<ReferenceLeaf> nested = <ReferenceLeaf>[];
+}
+
+class SelfReferenceBase<T> {
+ SelfReferenceBase();
+
+ @ForyField(id: 1, ref: true)
+ T? self;
+}
+
+@ForyStruct()
+final class InheritedSelfReference
+ extends SelfReferenceBase<InheritedSelfReference> {
+ InheritedSelfReference();
+}
+
+@ForyStruct()
+final class FlatSelfReference {
+ FlatSelfReference();
+
+ @ForyField(id: 1, ref: true)
+ FlatSelfReference? self;
+}
diff --git a/dart/packages/fory-test/pubspec.yaml b/dart/packages/fory-test/pubspec.yaml
index 204c72226..88fca2e3f 100644
--- a/dart/packages/fory-test/pubspec.yaml
+++ b/dart/packages/fory-test/pubspec.yaml
@@ -27,9 +27,10 @@ environment:
dependencies:
external_type_test_models: 1.0.0
fory: 1.5.0-dev
+ inheritance_test_models: 1.0.0
dev_dependencies:
- build_runner: '>=2.7.0 <3.0.0'
+ build_runner: ">=2.7.0 <3.0.0"
lints: ^6.1.0
test: ^1.31.1
vm_service: ^15.0.0
diff --git a/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart b/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart
index abe80edbf..2f6d4956e 100644
--- a/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart
+++ b/dart/packages/fory-test/test/cross_lang_test/xlang_test_main.dart
@@ -26,6 +26,7 @@ import 'package:fory/fory.dart';
import 'package:fory/src/util/hash_util.dart';
import 'package:fory_test/entity/xlang_test_models.dart';
import 'package:fory_test/model/external_serializers.dart';
+import 'package:fory_test/model/inheritance_models.dart';
String _dataFilePath() {
final path = Platform.environment['DATA_FILE'];
@@ -322,6 +323,43 @@ void _runExternalUser({int? id, String? name}) {
_writeFile(fory.serialize(user));
}
+void _registerInheritedXlangChild(Fory fory) {
+ InheritanceModelsForyModule.register(fory, XlangInheritedChild, id: 1101);
+}
+
+XlangInheritedChild _newInheritedXlangChild() {
+ return XlangInheritedChild()
+ ..childInt = 101
+ ..childFlag = true
+ ..childText = 'child'
+ ..setParentValues(parentInt: 202, parentFlag: false, parentText: 'parent');
+}
+
+void _verifyInheritedXlangChild(XlangInheritedChild value) {
+ if (value.childInt != 101 ||
+ value.parentInt != 202 ||
+ !value.childFlag ||
+ value.parentFlag ||
+ value.childText != 'child' ||
+ value.parentText != 'parent') {
+ throw StateError('Unexpected inherited xlang field values.');
+ }
+}
+
+void _roundTripInheritedXlangChild() {
+ final fory = _newFory();
+ _registerInheritedXlangChild(fory);
+ final value = fory.deserialize<XlangInheritedChild>(_readFile());
+ _verifyInheritedXlangChild(value);
+ _writeFile(fory.serialize(value));
+}
+
+void _writeInheritedXlangChild() {
+ final fory = _newFory();
+ _registerInheritedXlangChild(fory);
+ _writeFile(fory.serialize(_newInheritedXlangChild()));
+}
+
void _runCollectionElementRefOverride() {
final fory = _newFory();
registerXlangType(fory, RefOverrideElement, id: 701);
@@ -469,6 +507,12 @@ void _runCase(String caseName) {
case 'test_external_struct_name':
_runExternalUser(name: 'test.external_user');
return;
+ case 'test_inherited_flat_from_java':
+ _roundTripInheritedXlangChild();
+ return;
+ case 'test_inherited_flat_from_dart':
+ _writeInheritedXlangChild();
+ return;
case 'test_list':
case 'test_map':
case 'test_item':
diff --git a/dart/packages/fory-test/test/struct_inheritance_test.dart b/dart/packages/fory-test/test/struct_inheritance_test.dart
new file mode 100644
index 000000000..e210dc17f
--- /dev/null
+++ b/dart/packages/fory-test/test/struct_inheritance_test.dart
@@ -0,0 +1,338 @@
+/*
+ * 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.
+ */
+
+import 'package:fory/fory.dart';
+import 'package:fory_test/model/inheritance_models.dart';
+import 'package:test/test.dart';
+
+void _registerChild(Fory fory, {required bool byName}) {
+ InheritanceModelsForyModule.register(
+ fory,
+ CrossLibraryChild,
+ id: byName ? null : 401,
+ name: byName ? 'inheritance.CrossLibraryChild' : null,
+ );
+}
+
+void _registerReferenceLeaf(Fory fory) {
+ InheritanceModelsForyModule.register(fory, ReferenceLeaf, id: 402);
+}
+
+void _registerReferenceRoot(Fory fory, Type type) {
+ InheritanceModelsForyModule.register(
+ fory,
+ type,
+ name: 'inheritance.ReferenceRoot',
+ );
+}
+
+void _registerSelfRoot(Fory fory, Type type) {
+ InheritanceModelsForyModule.register(
+ fory,
+ type,
+ name: 'inheritance.SelfRoot',
+ );
+}
+
+void _registerCompatibleHierarchy(
+ Fory fory, {
+ required Type parentType,
+ required Type childType,
+}) {
+ InheritanceModelsForyModule.register(
+ fory,
+ parentType,
+ name: 'inheritance.CompatibleParent',
+ );
+ InheritanceModelsForyModule.register(
+ fory,
+ childType,
+ name: 'inheritance.CompatibleChild',
+ );
+}
+
+void main() {
+ group('ordinary struct inheritance', () {
+ for (final compatible in <bool>[false, true]) {
+ test('round trips private hierarchy compatible=$compatible', () {
+ final fory = Fory(compatible: compatible);
+ _registerChild(fory, byName: compatible);
+
+ final input =
+ CrossLibraryChild('base-final', 37, true)
+ ..setBasePrivateName('base-private')
+ ..setMiddlePrivateName('middle-private')
+ ..setMixinCount(9)
+ ..publicLabel = 'base-public'
+ ..publicMiddleCount = 12
+ ..childMutable = 'child';
+
+ final result = fory.deserialize<CrossLibraryChild>(
+ fory.serialize(input),
+ );
+
+ expect(result.ownerValue, 'base-final');
+ expect(result.middleValue, 37);
+ expect(result.childFinal, isTrue);
+ expect(result.basePrivateName, 'base-private');
+ expect(result.middlePrivateName, 'middle-private');
+ expect(result.mixinCount, 9);
+ expect(result.publicLabel, 'base-public');
+ expect(result.publicMiddleCount, 12);
+ expect(result.childMutable, 'child');
+ });
+ }
+
+ test('passes optional mutable state through construction', () {
+ final fory = Fory();
+ InheritanceModelsForyModule.register(fory, OptionalMutableChild, id: 405);
+
+ final result = fory.deserialize<OptionalMutableChild>(
+ fory.serialize(OptionalMutableChild(7, 19)),
+ );
+
+ expect(result.fixed, 7);
+ expect(result.inheritedMutable, 19);
+ expect(result.observedAtConstruction, 19);
+ });
+
+ test('analyzes optional flow for all-writable state', () {
+ final fory = Fory();
+ InheritanceModelsForyModule.register(
+ fory,
+ AllWritableOptionalChild,
+ id: 406,
+ );
+
+ final result = fory.deserialize<AllWritableOptionalChild>(
+ fory.serialize(AllWritableOptionalChild(23)),
+ );
+
+ expect(result.inheritedMutable, 23);
+ expect(result.observedAtConstruction, 23);
+ });
+
+ for (final compatible in <bool>[false, true]) {
+ test('matches flat and inherited schemas compatible=$compatible', () {
+ final privateFory = Fory(compatible: compatible);
+ final publicFory = Fory(compatible: compatible);
+ final flatFory = Fory(compatible: compatible);
+ InheritanceModelsForyModule.register(
+ privateFory,
+ XlangInheritedChild,
+ id: 404,
+ );
+ InheritanceModelsForyModule.register(
+ publicFory,
+ PublicXlangInheritedChild,
+ id: 404,
+ );
+ InheritanceModelsForyModule.register(
+ flatFory,
+ FlatXlangInheritedStruct,
+ id: 404,
+ );
+
+ final privateValue =
+ XlangInheritedChild()
+ ..childInt = 11
+ ..childFlag = true
+ ..childText = 'child'
+ ..setParentValues(
+ parentInt: 22,
+ parentFlag: false,
+ parentText: 'parent',
+ );
+ final publicValue =
+ PublicXlangInheritedChild()
+ ..childInt = 11
+ ..parentInt = 22
+ ..childFlag = true
+ ..parentFlag = false
+ ..childText = 'child'
+ ..parentText = 'parent';
+ final flatValue =
+ FlatXlangInheritedStruct()
+ ..childInt = 11
+ ..parentInt = 22
+ ..childFlag = true
+ ..parentFlag = false
+ ..childText = 'child'
+ ..parentText = 'parent';
+
+ final privateBytes = privateFory.serialize(privateValue);
+ final publicBytes = publicFory.serialize(publicValue);
+ final flatBytes = flatFory.serialize(flatValue);
+ expect(privateBytes, publicBytes);
+ expect(privateBytes, flatBytes);
+
+ final privateResult = privateFory.deserialize<XlangInheritedChild>(
+ privateBytes,
+ );
+ final publicResult = publicFory.deserialize<PublicXlangInheritedChild>(
+ publicBytes,
+ );
+ final flatResult = flatFory.deserialize<FlatXlangInheritedStruct>(
+ flatBytes,
+ );
+ expect(privateResult.parentInt, 22);
+ expect(privateResult.parentText, 'parent');
+ expect(publicResult.parentInt, 22);
+ expect(publicResult.parentText, 'parent');
+ expect(flatResult.parentInt, 22);
+ expect(flatResult.parentText, 'parent');
+ });
+ }
+ });
+
+ group('compatible inherited evolution', () {
+ test('reads added and missing inherited fields both directions', () {
+ final oldFory = Fory(compatible: true);
+ final newFory = Fory(compatible: true);
+ _registerCompatibleHierarchy(
+ oldFory,
+ parentType: CompatibleOldParent,
+ childType: CompatibleOldChild,
+ );
+ _registerCompatibleHierarchy(
+ newFory,
+ parentType: CompatibleNewParent,
+ childType: CompatibleNewChild,
+ );
+
+ final oldInput =
+ CompatibleOldChild()
+ ..inheritedStable = 'old-parent'
+ ..childStable = 'old-child';
+ final newResult = newFory.deserialize<CompatibleNewChild>(
+ oldFory.serialize(oldInput),
+ );
+ expect(newResult.inheritedStable, 'old-parent');
+ expect(newResult.inheritedAdded, 'new-default');
+ expect(newResult.childStable, 'old-child');
+
+ final newInput =
+ CompatibleNewChild()
+ ..inheritedStable = 'new-parent'
+ ..inheritedAdded = 'new-only'
+ ..childStable = 'new-child';
+ final oldResult = oldFory.deserialize<CompatibleOldChild>(
+ newFory.serialize(newInput),
+ );
+ expect(oldResult.inheritedStable, 'new-parent');
+ expect(oldResult.childStable, 'new-child');
+ });
+
+ test('registers each parent independently', () {
+ final oldFory = Fory(compatible: true);
+ InheritanceModelsForyModule.register(
+ oldFory,
+ CompatibleOldParent,
+ name: 'inheritance.CompatibleParent',
+ );
+ final oldParent = CompatibleOldParent()..inheritedStable = 'old-parent';
+ final oldResult = oldFory.deserialize<CompatibleOldParent>(
+ oldFory.serialize(oldParent),
+ );
+ expect(oldResult.inheritedStable, 'old-parent');
+
+ final newFory = Fory(compatible: true);
+ InheritanceModelsForyModule.register(
+ newFory,
+ CompatibleNewParent,
+ name: 'inheritance.CompatibleParent',
+ );
+ final newParent =
+ CompatibleNewParent()
+ ..inheritedStable = 'new-parent'
+ ..inheritedAdded = 'new-field';
+ final newResult = newFory.deserialize<CompatibleNewParent>(
+ newFory.serialize(newParent),
+ );
+ expect(newResult.inheritedStable, 'new-parent');
+ expect(newResult.inheritedAdded, 'new-field');
+ });
+ });
+
+ group('equivalent flat references', () {
+ for (final compatible in <bool>[false, true]) {
+ test('matches inherited shared refs compatible=$compatible', () {
+ final inheritedFory = Fory(compatible: compatible);
+ final flatFory = Fory(compatible: compatible);
+ _registerReferenceLeaf(inheritedFory);
+ _registerReferenceLeaf(flatFory);
+ _registerReferenceRoot(inheritedFory, InheritedReferenceModel);
+ _registerReferenceRoot(flatFory, FlatReferenceModel);
+
+ final inheritedLeaf = ReferenceLeaf()..label = 'shared';
+ final flatLeaf = ReferenceLeaf()..label = 'shared';
+ final inherited =
+ InheritedReferenceModel()
+ ..first = inheritedLeaf
+ ..second = inheritedLeaf
+ ..nested = <ReferenceLeaf>[inheritedLeaf, inheritedLeaf];
+ final flat =
+ FlatReferenceModel()
+ ..first = flatLeaf
+ ..second = flatLeaf
+ ..nested = <ReferenceLeaf>[flatLeaf, flatLeaf];
+
+ final inheritedBytes = inheritedFory.serialize(inherited);
+ final flatBytes = flatFory.serialize(flat);
+ expect(inheritedBytes, flatBytes);
+
+ final inheritedResult = inheritedFory
+ .deserialize<InheritedReferenceModel>(inheritedBytes);
+ final flatResult = flatFory.deserialize<FlatReferenceModel>(flatBytes);
+ expect(
+ identical(inheritedResult.first, inheritedResult.second),
+ isTrue,
+ );
+ expect(
+ identical(inheritedResult.first, inheritedResult.nested.first),
+ isTrue,
+ );
+ expect(identical(flatResult.first, flatResult.second), isTrue);
+ expect(identical(flatResult.first, flatResult.nested.first), isTrue);
+ });
+
+ test('matches inherited self refs compatible=$compatible', () {
+ final inheritedFory = Fory(compatible: compatible);
+ final flatFory = Fory(compatible: compatible);
+ _registerSelfRoot(inheritedFory, InheritedSelfReference);
+ _registerSelfRoot(flatFory, FlatSelfReference);
+
+ final inherited = InheritedSelfReference();
+ inherited.self = inherited;
+ final flat = FlatSelfReference();
+ flat.self = flat;
+
+ final inheritedBytes = inheritedFory.serialize(inherited);
+ final flatBytes = flatFory.serialize(flat);
+ expect(inheritedBytes, flatBytes);
+
+ final inheritedResult = inheritedFory
+ .deserialize<InheritedSelfReference>(inheritedBytes);
+ final flatResult = flatFory.deserialize<FlatSelfReference>(flatBytes);
+ expect(identical(inheritedResult, inheritedResult.self), isTrue);
+ expect(identical(flatResult, flatResult.self), isTrue);
+ });
+ }
+ });
+}
diff --git a/dart/packages/fory-test/tool/inheritance_smoke.dart b/dart/packages/fory-test/tool/inheritance_smoke.dart
new file mode 100644
index 000000000..9e85c0459
--- /dev/null
+++ b/dart/packages/fory-test/tool/inheritance_smoke.dart
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+import 'package:fory/fory.dart';
+import 'package:fory_test/model/inheritance_models.dart';
+
+void main() {
+ final fory = Fory();
+ InheritanceModelsForyModule.register(fory, CrossLibraryChild, id: 401);
+ final input =
+ CrossLibraryChild('base-final', 37, true)
+ ..setBasePrivateName('base-private')
+ ..setMiddlePrivateName('middle-private')
+ ..setMixinCount(9)
+ ..publicLabel = 'base-public'
+ ..publicMiddleCount = 12
+ ..childMutable = 'child';
+ final result = fory.deserialize<CrossLibraryChild>(fory.serialize(input));
+ if (result.ownerValue != 'base-final' ||
+ result.middleValue != 37 ||
+ !result.childFinal ||
+ result.basePrivateName != 'base-private' ||
+ result.middlePrivateName != 'middle-private' ||
+ result.mixinCount != 9 ||
+ result.publicLabel != 'base-public' ||
+ result.publicMiddleCount != 12 ||
+ result.childMutable != 'child') {
+ throw StateError('Inherited structural serializer round trip failed.');
+ }
+}
diff --git a/dart/packages/fory/CHANGELOG.md b/dart/packages/fory/CHANGELOG.md
index eeb9729eb..a7f2566b5 100644
--- a/dart/packages/fory/CHANGELOG.md
+++ b/dart/packages/fory/CHANGELOG.md
@@ -6,6 +6,18 @@
- Replace `ForyField.skip` with `ForyField.ignore`, remove `skip` from container
field annotations, and include ignored and discoverable external target
fields in graph-memory accounting.
+- Flatten ordinary `ForyStruct` superclass and applied-mixin storage into the
+ concrete child's generated schema. Public and same-library private fields use
+ direct access; cross-library private fields require
+ `exposePrivateFields: true` on a public boundary in each declaring library.
+- Reject hidden, inaccessible, unsupported, or unconstructable inherited
+ storage instead of omitting it. Non-ignored `final` and `late final` fields
+ now require a proven identity-preserving constructor path.
+- This inheritance correction is a breaking generated-schema change. Regenerate
+ all affected `.fory.dart` files; fixed-schema payloads written when inherited
+ fields were omitted are not supported by a compatibility reader. External
+ target declarations remain explicit, and inheritance does not change the
+ runtime reference protocol.
## 1.4.0
diff --git a/dart/packages/fory/README.md b/dart/packages/fory/README.md
index 9b4206790..cda678654 100644
--- a/dart/packages/fory/README.md
+++ b/dart/packages/fory/README.md
@@ -11,6 +11,7 @@ cases.
- Cross-language serialization with the Fory xlang format
- Dart VM/AOT, Flutter, and web platform support
- Generated serializers for annotated structs and enums
+- Flattened superclass and mixin storage for ordinary generated structs
- External structural serializers for classes owned by another package
- Compatible mode for schema evolution
- Optional reference tracking for shared and circular object graphs
@@ -87,9 +88,73 @@ void main() {
Generate the companion file before running the program:
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
+## Ordinary Struct Inheritance
+
+An ordinary `@ForyStruct()` includes all instance storage from its concrete
+superclass and applied-mixin chain. The concrete child has one flattened schema
+and one generated serializer; parent fields are globally ordered with child
+fields rather than encoded as a nested parent object.
+
+Public inherited fields and private inherited fields declared in the same Dart
+library need no annotation on the parent. Field metadata stays with the field
+declaration, and `@ForyField(ignore: true)` on that declaration is the only way
+to omit storage from the generated schema.
+
+Private fields declared in another Dart library require an explicit opt-in from
+that declaring library:
+
+```dart
+// package:model_owner/base.dart
+import 'package:fory/fory.dart';
+
+part 'base.fory.dart';
+
+@ForyStruct(exposePrivateFields: true)
+abstract class AccountBase {
+ AccountBase(String tenantId) : _tenantId = tenantId;
+
+ final String _tenantId;
+
+ String get tenantId => _tenantId;
+}
+```
+
+The concrete child uses the normal annotation:
+
+```dart
+// lib/account.dart
+import 'package:fory/fory.dart';
+import 'package:model_owner/base.dart';
+
+part 'account.fory.dart';
+
+@ForyStruct()
+final class Account extends AccountBase {
+ Account(String tenantId) : super(tenantId);
+}
+```
+
+Run code generation in the package that declares `AccountBase` before building
+a dependent package. The provider's published source must include its generated
+`.fory.dart` part. A barrel import is also valid when it re-exports both the
+public boundary and its generated access companion.
+
+`exposePrivateFields` controls only cross-library access to private state. It
+does not enable field discovery, affect same-library private fields, or grant a
+child permission to expose private state owned by another library. If private
+fields come from multiple libraries, each declaring library must opt in
+independently.
+
+Every non-ignored `final` or `late final` field must receive its decoded value
+unchanged through the concrete child's generative constructor chain. Fory
+accepts initializing formals, super formals, redirects, and direct constructor
+initializers that preserve the exact field value. A matching parameter name
+alone is not sufficient. Generation fails for inaccessible, hidden,
+unsupported, or unconstructable storage instead of silently dropping it.
+
## External-Type Serialization
Use `ForyStruct.target` when another package owns a class whose public schema
@@ -131,6 +196,9 @@ final decoded = fory.deserialize<third_party.User>(bytes);
Fields and nested `List`, `Set`, and `Map` values use the same target
registration. Select a public named generative constructor with
`constructor: 'name'` when the unnamed constructor is not appropriate.
+External declarations remain explicit schemas: they may list an accessible
+inherited target property, but Fory does not scan the external target's
+hierarchy automatically, and `exposePrivateFields` is not valid with `target`.
## Type Registration
@@ -198,17 +266,20 @@ class NodeList {
}
```
+Inherited field metadata enters this same reference analysis. Inheritance does
+not add a second reference owner or change the runtime reference protocol.
+
## Field Annotations
`@ForyField()` controls per-field serialization behavior:
-| Option | Description |
-| ---------- | ------------------------------------------------ |
-| `ignore` | Exclude the field from serialization |
-| `id` | Stable field ID for compatible-mode evolution |
-| `nullable` | Override nullability inference |
-| `ref` | Enable reference tracking for this field |
-| `dynamic` | Control whether runtime type metadata is written |
+| Option | Description |
+| ---------- | --------------------------------------------------- |
+| `ignore` | Exclude the declaring storage field from the schema |
+| `id` | Stable field ID for compatible-mode evolution |
+| `nullable` | Override nullability inference |
+| `ref` | Enable reference tracking for this field |
+| `dynamic` | Control whether runtime type metadata is written |
`type:` is the canonical override surface for nested field semantics:
@@ -224,7 +295,8 @@ Map<String, List<int?>> nested = <String, List<int?>>{};
## Manual Serializers
Use `Serializer<T>` when a type needs custom wire behavior, field conversion,
-or construction that an external structural serializer cannot express.
+or construction that generated ordinary or external structural serializers
+cannot prove.
```dart
import 'package:fory/fory.dart';
@@ -321,8 +393,9 @@ The main exported API includes:
- `Fory` — main serialization facade
- `Config` — Fory configuration
-- `ForyStruct`, including `target` and `constructor`, plus `ForyField`,
- `ListField`, `SetField`, and `MapField` — struct annotations
+- `ForyStruct`, including `target`, `constructor`, and
+ `exposePrivateFields`, plus `ForyField`, `ListField`, `SetField`, and
+ `MapField` — struct annotations
- `ForyUnion` — union type annotation
- `Serializer`, `UnionSerializer`, `EnumSerializer` — serializer base classes
- `Buffer`, `WriteContext`, `ReadContext` — low-level I/O
diff --git a/dart/packages/fory/lib/src/annotation/fory_field.dart b/dart/packages/fory/lib/src/annotation/fory_field.dart
index 6df0a0928..a9b82e51a 100644
--- a/dart/packages/fory/lib/src/annotation/fory_field.dart
+++ b/dart/packages/fory/lib/src/annotation/fory_field.dart
@@ -19,14 +19,35 @@
import 'package:fory/src/annotation/type_spec.dart';
+/// Configures serialization for the storage field on which it is declared.
+///
+/// Field metadata remains attached to its declaration when an ordinary
+/// `ForyStruct` inherits the field. Every discovered ordinary storage field is
+/// serialized unless that declaring field sets [ignore] to `true`.
final class ForyField {
+ /// Whether to omit this field from the serialized schema.
+ ///
+ /// This is the only supported way to omit an ordinary instance storage
+ /// field. Ignored storage still contributes to shallow graph-memory
+ /// accounting.
final bool ignore;
+
+ /// The stable numeric field identity used for schema evolution.
final int? id;
+
+ /// An optional override for the field's inferred nullability.
final bool? nullable;
+
+ /// Whether references through this field preserve object identity.
final bool ref;
+
+ /// Whether values carry their concrete runtime type.
final bool? dynamic;
+
+ /// An optional exact wire-type description.
final TypeSpec? type;
+ /// Creates field-level serialization metadata.
const ForyField({
this.ignore = false,
this.id,
diff --git a/dart/packages/fory/lib/src/annotation/fory_struct.dart b/dart/packages/fory/lib/src/annotation/fory_struct.dart
index 2812cfc7b..786aab085 100644
--- a/dart/packages/fory/lib/src/annotation/fory_struct.dart
+++ b/dart/packages/fory/lib/src/annotation/fory_struct.dart
@@ -17,7 +17,7 @@
* under the License.
*/
-/// Configures Fory struct code generation for the annotated class or [target].
+/// Configures Fory generation for an ordinary class or mixin, or for [target].
final class ForyStruct {
/// Whether the generated struct should use evolving field metadata.
///
@@ -39,6 +39,20 @@ final class ForyStruct {
/// with [target].
final String? constructor;
+ /// Whether this library exposes private hierarchy storage to child libraries.
+ ///
+ /// This option is valid only on a public ordinary hierarchy boundary. It
+ /// generates typed static access methods for non-ignored private storage
+ /// declared by this library and inherited through the boundary. It does not
+ /// control field discovery or same-library private access, and it cannot be
+ /// used with [target].
+ final bool exposePrivateFields;
+
/// Creates struct-level generation options.
- const ForyStruct({this.evolving = true, this.target, this.constructor});
+ const ForyStruct({
+ this.evolving = true,
+ this.target,
+ this.constructor,
+ this.exposePrivateFields = false,
+ });
}
diff --git a/dart/packages/fory/lib/src/codegen/fory_constructor_analysis.dart b/dart/packages/fory/lib/src/codegen/fory_constructor_analysis.dart
new file mode 100644
index 000000000..d817a983e
--- /dev/null
+++ b/dart/packages/fory/lib/src/codegen/fory_constructor_analysis.dart
@@ -0,0 +1,874 @@
+/*
+ * 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.
+ */
+
+part of 'fory_generator.dart';
+
+final class _OrdinaryConstructorAnalyzer {
+ final BuildStep buildStep;
+
+ final Map<ConstructorElement, Future<AstNode?>> _astByConstructor =
+ Map<ConstructorElement, Future<AstNode?>>.identity();
+ final List<_ConstructorFrame> _frames = <_ConstructorFrame>[];
+
+ _OrdinaryConstructorAnalyzer({required this.buildStep});
+
+ Future<_ConstructionModel> build({
+ required ClassElement declaration,
+ required String targetTypeLiteral,
+ required ConstructorElement constructor,
+ required List<_GeneratedFieldSpec> fields,
+ }) async {
+ if (constructor.formalParameters.isEmpty) {
+ var mutableConstruction = true;
+ for (final field in fields) {
+ if (!field.writable) {
+ mutableConstruction = false;
+ break;
+ }
+ }
+ if (mutableConstruction) {
+ return const _ConstructionModel.mutable(constructorName: null);
+ }
+ }
+
+ for (final field in fields) {
+ if (field.declaration.isFinal && field.declaration.hasInitializer) {
+ throw InvalidGenerationSourceError(
+ 'Final field ${_fieldLabel(field)} has a declaration initializer '
+ 'and cannot receive its decoded value from '
+ '$targetTypeLiteral(). Mark the field with '
+ '@ForyField(ignore: true), remove the initializer and add an '
+ 'identity-preserving constructor path, or use a manual serializer.',
+ element: field.declaration,
+ );
+ }
+ }
+
+ _frames.clear();
+ final active = <_ConstructorKey>{};
+ final rootFrame = await _buildFrame(
+ constructor,
+ fields,
+ declaration,
+ active,
+ );
+ final reachableByRoot = <_ParameterNode, Set<_GeneratedFieldSpec>>{
+ for (final root in rootFrame.parameters) root: _reachableFields(root),
+ };
+ final bindingByRoot = <_ParameterNode, _GeneratedFieldSpec>{};
+ final rootByArgumentField =
+ Map<_GeneratedFieldSpec, _ParameterNode>.identity();
+
+ for (final field in fields) {
+ if (field.writable) {
+ continue;
+ }
+ _ParameterNode? root;
+ for (final candidate in rootFrame.parameters) {
+ if (!reachableByRoot[candidate]!.contains(field)) {
+ continue;
+ }
+ if (root != null) {
+ throw InvalidGenerationSourceError(
+ 'Final field ${_fieldLabel(field)} is reached by multiple '
+ 'parameters of $targetTypeLiteral(). A serialized final field '
+ 'must have exactly one identity-preserving constructor root.',
+ element: field.declaration,
+ );
+ }
+ root = candidate;
+ }
+ if (root == null) {
+ _throwMissingFinalFlow(declaration, targetTypeLiteral, field);
+ }
+ final existing = bindingByRoot[root];
+ if (existing != null && !identical(existing, field)) {
+ throw InvalidGenerationSourceError(
+ 'Constructor parameter ${root.parameter.displayName} on '
+ '$targetTypeLiteral initializes multiple non-writable fields '
+ '${_fieldLabel(existing)} and ${_fieldLabel(field)}. One decoded '
+ 'constructor argument may restore at most one final field.',
+ element: declaration,
+ );
+ }
+ bindingByRoot[root] = field;
+ rootByArgumentField[field] = root;
+ }
+
+ for (final root in rootFrame.parameters) {
+ if (bindingByRoot.containsKey(root)) {
+ continue;
+ }
+ _GeneratedFieldSpec? candidate;
+ List<String>? ambiguousLabels;
+ for (final field in reachableByRoot[root]!) {
+ if (!field.writable) {
+ continue;
+ }
+ if (candidate == null) {
+ candidate = field;
+ continue;
+ }
+ ambiguousLabels ??= <String>[_fieldLabel(candidate)];
+ ambiguousLabels.add(_fieldLabel(field));
+ }
+ if (candidate == null) {
+ if (root.parameter.isRequired) {
+ _throwMissingRequiredSource(
+ declaration,
+ targetTypeLiteral,
+ root,
+ fields,
+ );
+ }
+ continue;
+ }
+ if (ambiguousLabels != null) {
+ throw InvalidGenerationSourceError(
+ 'Constructor parameter ${root.parameter.displayName} on '
+ '$targetTypeLiteral has multiple writable identity-flow sources: '
+ '${ambiguousLabels.join(', ')}. Select a constructor '
+ 'whose parameter flow identifies exactly one serialized field.',
+ element: declaration,
+ );
+ }
+ final field = candidate;
+ final existingRoot = rootByArgumentField[field];
+ if (existingRoot != null && !identical(existingRoot, root)) {
+ throw InvalidGenerationSourceError(
+ 'Serialized field ${_fieldLabel(field)} would supply constructor '
+ 'parameters ${existingRoot.parameter.displayName} and '
+ '${root.parameter.displayName} on $targetTypeLiteral. A field may '
+ 'supply at most one constructor argument.',
+ element: field.declaration,
+ );
+ }
+ bindingByRoot[root] = field;
+ rootByArgumentField[field] = root;
+ }
+
+ final arguments = <_ConstructorArgumentSpec>[];
+ var omittedOptionalPositional = false;
+ for (final root in rootFrame.parameters) {
+ final parameter = root.parameter;
+ final field = bindingByRoot[root];
+ if (field == null) {
+ if (parameter.isOptionalPositional) {
+ omittedOptionalPositional = true;
+ }
+ continue;
+ }
+ if (parameter.isPositional && omittedOptionalPositional) {
+ throw InvalidGenerationSourceError(
+ 'Constructor $targetTypeLiteral() cannot pass parameter '
+ '${parameter.displayName} after an omitted optional positional '
+ 'parameter. Use named parameters, remove the positional gap, mark '
+ 'the final field with @ForyField(ignore: true), or use a manual '
+ 'serializer.',
+ element: declaration,
+ );
+ }
+ arguments.add(
+ _ConstructorArgumentSpec(
+ field: field,
+ parameterName: parameter.displayName,
+ named: parameter.isNamed,
+ ),
+ );
+ }
+
+ if (bindingByRoot.isEmpty) {
+ return const _ConstructionModel.mutable(constructorName: null);
+ }
+
+ final postConstructionFields = Set<_GeneratedFieldSpec>.identity();
+ for (final field in fields) {
+ if (field.writable && !rootByArgumentField.containsKey(field)) {
+ postConstructionFields.add(field);
+ }
+ }
+ return _ConstructionModel.constructor(
+ constructorName: null,
+ arguments: arguments,
+ postConstructionFields: postConstructionFields,
+ );
+ }
+
+ Future<_ConstructorFrame> _buildFrame(
+ ConstructorElement constructor,
+ List<_GeneratedFieldSpec> fields,
+ ClassElement declaration,
+ Set<_ConstructorKey> active,
+ ) async {
+ final key = _ConstructorKey(
+ constructor.baseElement,
+ constructor.returnType,
+ );
+ if (active.contains(key)) {
+ throw InvalidGenerationSourceError(
+ 'Constructor redirection or super-constructor flow for '
+ '${declaration.displayName} is cyclic at '
+ '${_constructorLabel(constructor)}.',
+ element: declaration,
+ );
+ }
+ final frame = _ConstructorFrame(constructor);
+ _frames.add(frame);
+ active.add(key);
+ try {
+ final redirected = constructor.redirectedConstructor;
+ final superConstructor = constructor.superConstructor;
+ if (redirected != null && superConstructor != null) {
+ throw InvalidGenerationSourceError(
+ 'Constructor ${_constructorLabel(constructor)} resolves both a '
+ 'redirect target and a super constructor, so Fory cannot prove one '
+ 'construction path.',
+ element: declaration,
+ );
+ }
+ final nextConstructor = redirected ?? superConstructor;
+ final nextFrame =
+ nextConstructor == null || _isObjectConstructor(nextConstructor)
+ ? null
+ : await _buildFrame(nextConstructor, fields, declaration, active);
+
+ _addSummaryEdges(frame, nextFrame, fields);
+ if (_isMixinApplicationConstructor(constructor)) {
+ _addMixinForwardingEdges(frame, nextFrame, declaration);
+ } else if (_isDeclaredConstructor(constructor)) {
+ // Summary elements completely prove a leaf constructor only when it
+ // has no optional parameters and every required argument and final
+ // field already has an exact storage edge. An explicit initializer
+ // may connect an optional parameter to mutable storage that must
+ // receive its decoded value during construction.
+ if (nextFrame != null || !_summaryProvesLeaf(frame, fields)) {
+ final node = await _astFor(constructor);
+ if (node is ConstructorDeclaration) {
+ _addAstEdges(
+ frame,
+ nextFrame,
+ redirected != null
+ ? _ConstructorHop.redirect
+ : _ConstructorHop.superCall,
+ node,
+ fields,
+ );
+ } else {
+ frame.unavailableProof =
+ node == null
+ ? 'resolved source AST is unavailable'
+ : 'resolved node ${node.runtimeType} is not a supported '
+ 'constructor declaration';
+ }
+ }
+ } else if (frame.parameters.isNotEmpty) {
+ frame.unavailableProof =
+ 'synthetic constructor parameters do not expose an exact '
+ 'forwarding relationship';
+ }
+ return frame;
+ } finally {
+ active.remove(key);
+ }
+ }
+
+ bool _summaryProvesLeaf(
+ _ConstructorFrame frame,
+ List<_GeneratedFieldSpec> fields,
+ ) {
+ for (final parameter in frame.parameters) {
+ if (parameter.parameter.isOptional || parameter.fieldTargets.isEmpty) {
+ return false;
+ }
+ }
+ for (final field in fields) {
+ if (field.writable) {
+ continue;
+ }
+ var hasFieldTarget = false;
+ for (final parameter in frame.parameters) {
+ if (parameter.fieldTargets.contains(field)) {
+ hasFieldTarget = true;
+ break;
+ }
+ }
+ if (!hasFieldTarget) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ void _addSummaryEdges(
+ _ConstructorFrame frame,
+ _ConstructorFrame? nextFrame,
+ List<_GeneratedFieldSpec> fields,
+ ) {
+ for (final source in frame.parameters) {
+ final parameter = source.parameter;
+ if (parameter is FieldFormalParameterElement) {
+ final fieldElement = parameter.field;
+ if (fieldElement == null) {
+ frame.recordIssue(
+ 'field formal ${parameter.displayName} has no resolved storage '
+ 'field',
+ );
+ } else {
+ final field = _fieldFor(fieldElement, frame.constructor, fields);
+ if (field != null) {
+ _addFieldEdge(frame, source, field);
+ }
+ }
+ }
+ if (parameter is SuperFormalParameterElement) {
+ final targetElement = parameter.superConstructorParameter;
+ if (nextFrame == null || targetElement == null) {
+ frame.recordIssue(
+ 'super formal ${parameter.displayName} has no resolved target '
+ 'parameter',
+ );
+ continue;
+ }
+ final target = nextFrame.parameterFor(targetElement);
+ if (target == null) {
+ frame.recordIssue(
+ 'super formal ${parameter.displayName} does not identify a '
+ 'parameter on ${_constructorLabel(nextFrame.constructor)}',
+ );
+ continue;
+ }
+ _addParameterEdge(frame, source, target);
+ }
+ }
+ }
+
+ void _addMixinForwardingEdges(
+ _ConstructorFrame frame,
+ _ConstructorFrame? nextFrame,
+ ClassElement declaration,
+ ) {
+ if (nextFrame == null ||
+ frame.parameters.length != nextFrame.parameters.length) {
+ throw InvalidGenerationSourceError(
+ 'Synthetic mixin-application constructor '
+ '${_constructorLabel(frame.constructor)} does not have the exact '
+ 'forwarding signature of its super constructor.',
+ element: declaration,
+ );
+ }
+ for (var index = 0; index < frame.parameters.length; index += 1) {
+ final source = frame.parameters[index];
+ final target = nextFrame.parameters[index];
+ final sourceParameter = source.parameter;
+ final targetParameter = target.parameter;
+ final sameKind =
+ sourceParameter.isNamed == targetParameter.isNamed &&
+ sourceParameter.isRequired == targetParameter.isRequired &&
+ sourceParameter.isOptional == targetParameter.isOptional &&
+ sourceParameter.isPositional == targetParameter.isPositional;
+ final sameName =
+ !sourceParameter.isNamed ||
+ sourceParameter.displayName == targetParameter.displayName;
+ if (!sameKind ||
+ !sameName ||
+ sourceParameter.type != targetParameter.type) {
+ throw InvalidGenerationSourceError(
+ 'Synthetic mixin-application constructor parameter '
+ '${sourceParameter.displayName} on '
+ '${_constructorLabel(frame.constructor)} does not exactly forward '
+ 'the corresponding super-constructor parameter.',
+ element: declaration,
+ );
+ }
+ source.addParameterTarget(target);
+ }
+ }
+
+ void _addAstEdges(
+ _ConstructorFrame frame,
+ _ConstructorFrame? nextFrame,
+ _ConstructorHop hop,
+ ConstructorDeclaration node,
+ List<_GeneratedFieldSpec> fields,
+ ) {
+ final declaredConstructor = node.declaredFragment?.element;
+ if (declaredConstructor == null ||
+ !identical(
+ declaredConstructor.baseElement,
+ frame.constructor.baseElement,
+ )) {
+ frame.recordIssue(
+ 'resolved constructor AST does not identify '
+ '${_constructorLabel(frame.constructor)}',
+ );
+ return;
+ }
+
+ RedirectingConstructorInvocation? redirectInvocation;
+ SuperConstructorInvocation? superInvocation;
+ for (final initializer in node.initializers) {
+ if (initializer is ConstructorFieldInitializer) {
+ _addFieldInitializerEdge(frame, initializer, fields);
+ } else if (initializer is RedirectingConstructorInvocation) {
+ if (redirectInvocation != null) {
+ frame.recordIssue('multiple redirecting constructor invocations');
+ }
+ redirectInvocation = initializer;
+ } else if (initializer is SuperConstructorInvocation) {
+ if (superInvocation != null) {
+ frame.recordIssue('multiple super-constructor invocations');
+ }
+ superInvocation = initializer;
+ }
+ }
+
+ if (nextFrame == null) {
+ return;
+ }
+ switch (hop) {
+ case _ConstructorHop.redirect:
+ final invocation = redirectInvocation;
+ if (invocation == null) {
+ frame.recordIssue(
+ 'redirect target ${_constructorLabel(nextFrame.constructor)} '
+ 'requires an explicit resolved invocation',
+ );
+ return;
+ }
+ _addInvocationEdges(
+ frame,
+ nextFrame,
+ invocation.element,
+ invocation.argumentList.arguments,
+ );
+ case _ConstructorHop.superCall:
+ final invocation = superInvocation;
+ if (invocation == null) {
+ return;
+ }
+ _addInvocationEdges(
+ frame,
+ nextFrame,
+ invocation.element,
+ invocation.argumentList.arguments,
+ );
+ }
+ }
+
+ void _addFieldInitializerEdge(
+ _ConstructorFrame frame,
+ ConstructorFieldInitializer initializer,
+ List<_GeneratedFieldSpec> fields,
+ ) {
+ final fieldElement = initializer.fieldName.element;
+ if (fieldElement is! FieldElement) {
+ frame.recordIssue(
+ 'field initializer ${initializer.fieldName.name} has no resolved '
+ 'storage field',
+ );
+ return;
+ }
+ final field = _fieldFor(fieldElement, frame.constructor, fields);
+ if (field == null) {
+ return;
+ }
+ final source = _directParameter(initializer.expression, frame);
+ if (source == null) {
+ if (!field.writable) {
+ frame.recordIssue(
+ 'initializer for ${_fieldLabel(field)} transforms or does not '
+ 'directly reference a constructor parameter',
+ );
+ }
+ return;
+ }
+ _addFieldEdge(frame, source, field);
+ }
+
+ void _addInvocationEdges(
+ _ConstructorFrame frame,
+ _ConstructorFrame targetFrame,
+ ConstructorElement? invokedConstructor,
+ Iterable<AstNode> arguments,
+ ) {
+ if (invokedConstructor == null ||
+ !identical(
+ invokedConstructor.baseElement,
+ targetFrame.constructor.baseElement,
+ )) {
+ frame.recordIssue(
+ 'resolved invocation does not identify '
+ '${_constructorLabel(targetFrame.constructor)}',
+ );
+ return;
+ }
+ var positionalIndex = 0;
+ for (final argument in arguments) {
+ final firstToken = argument.beginToken;
+ final named = firstToken.next?.lexeme == ':';
+ final target = _invocationTarget(
+ targetFrame,
+ firstToken.lexeme,
+ named,
+ positionalIndex,
+ );
+ if (!named) {
+ positionalIndex += 1;
+ }
+ if (target == null) {
+ frame.recordIssue(
+ 'constructor argument has no resolved target parameter',
+ );
+ continue;
+ }
+ final value = _invocationValue(argument, named);
+ if (value == null) {
+ frame.recordIssue(
+ 'constructor argument has no resolved value expression',
+ );
+ continue;
+ }
+ final source = _directParameter(value, frame);
+ if (source == null) {
+ frame.recordIssue(
+ 'argument for ${target.parameter.displayName} transforms or does '
+ 'not directly reference a constructor parameter',
+ );
+ continue;
+ }
+ _addParameterEdge(frame, source, target);
+ }
+ }
+
+ _ParameterNode? _invocationTarget(
+ _ConstructorFrame frame,
+ String argumentName,
+ bool named,
+ int positionalIndex,
+ ) {
+ var currentPositionalIndex = 0;
+ for (final parameter in frame.parameters) {
+ if (named) {
+ if (parameter.parameter.isNamed &&
+ parameter.parameter.displayName == argumentName) {
+ return parameter;
+ }
+ continue;
+ }
+ if (!parameter.parameter.isPositional) {
+ continue;
+ }
+ if (currentPositionalIndex == positionalIndex) {
+ return parameter;
+ }
+ currentPositionalIndex += 1;
+ }
+ return null;
+ }
+
+ Expression? _invocationValue(AstNode argument, bool named) {
+ if (!named) {
+ return argument is Expression ? argument : null;
+ }
+ // Supported analyzer versions use different public node types for named
+ // arguments. Dart grammar still gives both representations exactly one
+ // direct value expression after the name and colon.
+ Expression? value;
+ for (final child in argument.childEntities) {
+ if (child is! Expression) {
+ continue;
+ }
+ if (value != null) {
+ return null;
+ }
+ value = child;
+ }
+ return value;
+ }
+
+ void _addFieldEdge(
+ _ConstructorFrame frame,
+ _ParameterNode source,
+ _GeneratedFieldSpec field,
+ ) {
+ if (source.parameter.type != field.type) {
+ frame.recordIssue(
+ 'parameter ${source.parameter.displayName} has type '
+ '${source.parameter.type.getDisplayString()}, but '
+ '${_fieldLabel(field)} has type ${field.type.getDisplayString()}',
+ );
+ return;
+ }
+ source.addFieldTarget(field);
+ }
+
+ void _addParameterEdge(
+ _ConstructorFrame frame,
+ _ParameterNode source,
+ _ParameterNode target,
+ ) {
+ if (source.parameter.type != target.parameter.type) {
+ frame.recordIssue(
+ 'parameter ${source.parameter.displayName} on '
+ '${_constructorLabel(frame.constructor)} has type '
+ '${source.parameter.type.getDisplayString()}, but target parameter '
+ '${target.parameter.displayName} has type '
+ '${target.parameter.type.getDisplayString()}',
+ );
+ return;
+ }
+ source.addParameterTarget(target);
+ }
+
+ _GeneratedFieldSpec? _fieldFor(
+ FieldElement fieldElement,
+ ConstructorElement constructor,
+ List<_GeneratedFieldSpec> fields,
+ ) {
+ final fieldBase = fieldElement.baseElement;
+ final ownerBase = fieldBase.enclosingElement.baseElement;
+ final constructorOwner = constructor.enclosingElement.baseElement;
+ _GeneratedFieldSpec? match;
+ for (final field in fields) {
+ if (!identical(field.declaration.baseElement, fieldBase) ||
+ !identical(
+ field.declaration.enclosingElement.baseElement,
+ ownerBase,
+ ) ||
+ !identical(ownerBase, constructorOwner)) {
+ continue;
+ }
+ if (match != null) {
+ throw InvalidGenerationSourceError(
+ 'Constructor ${_constructorLabel(constructor)} cannot identify one '
+ 'applied storage slot for ${fieldBase.displayName}.',
+ element: fieldBase,
+ );
+ }
+ match = field;
+ }
+ return match;
+ }
+
+ _ParameterNode? _directParameter(
+ Expression expression,
+ _ConstructorFrame frame,
+ ) {
+ final unwrapped = expression.unParenthesized;
+ if (unwrapped is! SimpleIdentifier) {
+ return null;
+ }
+ final element = unwrapped.element;
+ return element is FormalParameterElement
+ ? frame.parameterFor(element)
+ : null;
+ }
+
+ Set<_GeneratedFieldSpec> _reachableFields(_ParameterNode root) {
+ final fields = Set<_GeneratedFieldSpec>.identity();
+ final visited = Set<_ParameterNode>.identity();
+
+ void visit(_ParameterNode parameter) {
+ if (!visited.add(parameter)) {
+ return;
+ }
+ fields.addAll(parameter.fieldTargets);
+ for (final target in parameter.parameterTargets) {
+ visit(target);
+ }
+ }
+
+ visit(root);
+ return fields;
+ }
+
+ Future<AstNode?> _astFor(ConstructorElement constructor) {
+ final base = constructor.baseElement;
+ final cached = _astByConstructor[base];
+ if (cached != null) {
+ return cached;
+ }
+ final ast = buildStep.resolver.astNodeFor(
+ base.firstFragment,
+ resolve: true,
+ );
+ _astByConstructor[base] = ast;
+ return ast;
+ }
+
+ Never _throwMissingFinalFlow(
+ ClassElement declaration,
+ String targetTypeLiteral,
+ _GeneratedFieldSpec field,
+ ) {
+ final proofProblem = _firstProofProblem();
+ final detail =
+ proofProblem == null
+ ? ''
+ : ' Constructor proof could not use $proofProblem.';
+ throw InvalidGenerationSourceError(
+ 'Final field ${_fieldLabel(field)} is not initialized from a parameter '
+ 'of selected constructor $targetTypeLiteral(). A serialized final field '
+ 'must receive its decoded value unchanged through a field formal, '
+ 'constructor initializer, redirect, or super-constructor forwarding.'
+ '$detail Mark the field with @ForyField(ignore: true), add an exact '
+ 'identity path, or use a manual serializer.',
+ element: field.declaration,
+ );
+ }
+
+ Never _throwMissingRequiredSource(
+ ClassElement declaration,
+ String targetTypeLiteral,
+ _ParameterNode root,
+ List<_GeneratedFieldSpec> fields,
+ ) {
+ _GeneratedFieldSpec? sameName;
+ for (final field in fields) {
+ if (field.name == root.parameter.displayName && field.writable) {
+ sameName = field;
+ break;
+ }
+ }
+ final nameDetail =
+ sameName == null
+ ? ''
+ : ' A same-name field ${_fieldLabel(sameName)} exists, but name '
+ 'equality is not constructor identity proof.';
+ final proofProblem = _firstProofProblem();
+ final proofDetail =
+ proofProblem == null
+ ? ''
+ : ' Constructor proof could not use $proofProblem.';
+ throw InvalidGenerationSourceError(
+ 'Required constructor parameter ${root.parameter.displayName} on '
+ '$targetTypeLiteral has no serialized field connected by an exact '
+ 'identity-preserving path.$nameDetail$proofDetail Add a direct '
+ 'parameter-to-field flow, select a zero-required-argument constructor, '
+ 'or use a manual serializer.',
+ element: declaration,
+ );
+ }
+
+ String? _firstProofProblem() {
+ String? firstIssue;
+ for (final frame in _frames) {
+ final unavailable = frame.unavailableProof;
+ if (unavailable != null) {
+ return '$unavailable for ${_constructorLabel(frame.constructor)}';
+ }
+ final issue = frame.firstIssue;
+ if (firstIssue == null && issue != null) {
+ firstIssue = '$issue in ${_constructorLabel(frame.constructor)}';
+ }
+ }
+ return firstIssue;
+ }
+
+ bool _isDeclaredConstructor(ConstructorElement constructor) =>
+ constructor.baseElement.nonSynthetic is ConstructorElement;
+
+ bool _isMixinApplicationConstructor(ConstructorElement constructor) {
+ final owner = constructor.enclosingElement;
+ return owner is ClassElement && owner.isMixinApplication;
+ }
+
+ bool _isObjectConstructor(ConstructorElement constructor) {
+ final owner = constructor.enclosingElement;
+ return owner is ClassElement && owner.isDartCoreObject;
+ }
+
+ String _constructorLabel(ConstructorElement constructor) =>
+ '${constructor.enclosingElement.displayName}.'
+ '${constructor.displayName}';
+
+ String _fieldLabel(_GeneratedFieldSpec field) =>
+ '${field.declaration.enclosingElement.displayName}.${field.name}';
+}
+
+enum _ConstructorHop { redirect, superCall }
+
+final class _ConstructorKey {
+ final ConstructorElement declaration;
+ final InterfaceType returnType;
+
+ const _ConstructorKey(this.declaration, this.returnType);
+
+ @override
+ int get hashCode => Object.hash(identityHashCode(declaration), returnType);
+
+ @override
+ bool operator ==(Object other) =>
+ other is _ConstructorKey &&
+ identical(declaration, other.declaration) &&
+ returnType == other.returnType;
+}
+
+final class _ConstructorFrame {
+ final ConstructorElement constructor;
+ final List<_ParameterNode> parameters;
+ String? firstIssue;
+ String? unavailableProof;
+
+ _ConstructorFrame(this.constructor)
+ : parameters = <_ParameterNode>[
+ for (final parameter in constructor.formalParameters)
+ _ParameterNode(parameter),
+ ];
+
+ void recordIssue(String issue) {
+ firstIssue ??= issue;
+ }
+
+ _ParameterNode? parameterFor(FormalParameterElement element) {
+ final base = element.baseElement;
+ for (final parameter in parameters) {
+ if (identical(parameter.parameter.baseElement, base)) {
+ return parameter;
+ }
+ }
+ return null;
+ }
+}
+
+final class _ParameterNode {
+ final FormalParameterElement parameter;
+ final List<_ParameterNode> parameterTargets = <_ParameterNode>[];
+ final List<_GeneratedFieldSpec> fieldTargets = <_GeneratedFieldSpec>[];
+
+ _ParameterNode(this.parameter);
+
+ void addParameterTarget(_ParameterNode target) {
+ for (final existing in parameterTargets) {
+ if (identical(existing, target)) {
+ return;
+ }
+ }
+ parameterTargets.add(target);
+ }
+
+ void addFieldTarget(_GeneratedFieldSpec target) {
+ for (final existing in fieldTargets) {
+ if (identical(existing, target)) {
+ return;
+ }
+ }
+ fieldTargets.add(target);
+ }
+}
diff --git a/dart/packages/fory/lib/src/codegen/fory_generator.dart b/dart/packages/fory/lib/src/codegen/fory_generator.dart
index 710c02b90..619b5202e 100644
--- a/dart/packages/fory/lib/src/codegen/fory_generator.dart
+++ b/dart/packages/fory/lib/src/codegen/fory_generator.dart
@@ -18,7 +18,9 @@
*/
import 'dart:async';
+import 'dart:convert';
+import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
@@ -27,6 +29,8 @@ import 'package:build/build.dart';
import 'package:fory/fory.dart';
import 'package:source_gen/source_gen.dart';
+part 'fory_constructor_analysis.dart';
+
class DebugGeneratedFieldTypeSpec {
const DebugGeneratedFieldTypeSpec({
required this.typeLiteral,
@@ -86,16 +90,42 @@ final class ForyGenerator extends Generator {
inPackage: 'fory',
);
+ late LibraryElement _sourceLibrary;
final Map<Element, String?> _importPrefixByElement = <Element, String?>{};
+ final Set<Element> _resolvedImportElements = <Element>{};
+ final Map<InterfaceType, _HierarchyStorage> _hierarchyStorageByType =
+ <InterfaceType, _HierarchyStorage>{};
+ final Map<InterfaceElement, _StructOptions> _structOptionsByElement =
+ <InterfaceElement, _StructOptions>{};
+ Map<LibraryElement, String>? _canonicalLibraryUris;
+
+ ForyGenerator();
+
+ ForyGenerator._forInput();
@override
- FutureOr<String> generate(LibraryReader library, BuildStep buildStep) {
- _buildImportPrefixMap(library);
- final annotatedClasses = library.classes
- .where((element) => _foryStructChecker.hasAnnotationOf(element))
- .toList(growable: false);
- final enumElements = library.enums.toList(growable: false);
- if (annotatedClasses.isEmpty && enumElements.isEmpty) {
+ Future<String> generate(LibraryReader library, BuildStep buildStep) {
+ return ForyGenerator._forInput()._generate(library, buildStep);
+ }
+
+ Future<String> _generate(LibraryReader library, BuildStep buildStep) async {
+ _sourceLibrary = library.element;
+ final annotatedClasses = <ClassElement>[];
+ for (final element in library.classes) {
+ if (_foryStructChecker.hasAnnotationOf(element)) {
+ annotatedClasses.add(element);
+ }
+ }
+ final annotatedMixins = <MixinElement>[];
+ for (final element in library.element.mixins) {
+ if (_foryStructChecker.hasAnnotationOf(element)) {
+ annotatedMixins.add(element);
+ }
+ }
+ final enumElements = library.enums;
+ if (annotatedClasses.isEmpty &&
+ annotatedMixins.isEmpty &&
+ enumElements.isEmpty) {
return '';
}
@@ -106,14 +136,58 @@ final class ForyGenerator extends Generator {
final emitRegistrationHelper =
!_declaresGeneratedApiOwner(library, buildStep, generatedApiName);
- final enumSpecs = enumElements.map(_analyzeEnum).toList(growable: false);
+ final enumSpecs = <_GeneratedEnumSpec>[];
+ for (final element in enumElements) {
+ enumSpecs.add(_analyzeEnum(element));
+ }
final structSpecs = <_GeneratedStructSpec>[];
+ final companionSpecs = <_PrivateAccessCompanionSpec>[];
+ final constructorAnalyzer = _OrdinaryConstructorAnalyzer(
+ buildStep: buildStep,
+ );
for (final element in annotatedClasses) {
- final structSpec = _analyzeStruct(element);
- final duplicate =
- structSpecs
- .where((existing) => existing.targetType == structSpec.targetType)
- .firstOrNull;
+ final options = _structOptions(element);
+ final ownsSerializer =
+ options.target != null ||
+ (_ownsOrdinarySerializer(element) && element.typeParameters.isEmpty);
+ if (!ownsSerializer) {
+ _validateProviderOnly(element, options);
+ }
+ if (options.exposePrivateFields) {
+ if (options.target != null) {
+ throw InvalidGenerationSourceError(
+ 'ForyStruct.exposePrivateFields cannot be used with '
+ 'ForyStruct.target. External structural declarations retain an '
+ 'explicit field model.',
+ element: element,
+ );
+ }
+ companionSpecs.add(
+ await _analyzePrivateAccessCompanion(element, buildStep),
+ );
+ }
+ if (!ownsSerializer) {
+ continue;
+ }
+ final annotatedTarget = options.target;
+ final _GeneratedStructSpec structSpec;
+ if (annotatedTarget == null) {
+ structSpec = await _analyzeOrdinaryStruct(
+ element,
+ options,
+ constructorAnalyzer,
+ buildStep,
+ );
+ } else {
+ structSpec = _analyzeExternalStruct(element, options, annotatedTarget);
+ }
+ _GeneratedStructSpec? duplicate;
+ for (final existing in structSpecs) {
+ if (existing.targetType == structSpec.targetType) {
+ duplicate = existing;
+ break;
+ }
+ }
if (duplicate != null) {
throw InvalidGenerationSourceError(
'Fory struct declarations ${duplicate.name} and ${structSpec.name} '
@@ -124,6 +198,13 @@ final class ForyGenerator extends Generator {
}
structSpecs.add(structSpec);
}
+ for (final element in annotatedMixins) {
+ final options = _structOptions(element);
+ _validateProviderOnly(element, options);
+ companionSpecs.add(
+ await _analyzePrivateAccessCompanion(element, buildStep),
+ );
+ }
final output =
StringBuffer()
..writeln(
@@ -134,17 +215,22 @@ final class ForyGenerator extends Generator {
for (final enumSpec in enumSpecs) {
_writeEnum(output, enumSpec);
}
+ for (final companionSpec in companionSpecs) {
+ _writePrivateAccessCompanion(output, companionSpec);
+ }
for (final structSpec in structSpecs) {
_writeStruct(output, structSpec);
}
- _writeGeneratedSupport(
- output,
- enumSpecs: enumSpecs,
- structSpecs: structSpecs,
- generatedApiName: generatedApiName,
- emitRegistrationHelper: emitRegistrationHelper,
- );
+ if (enumSpecs.isNotEmpty || structSpecs.isNotEmpty) {
+ _writeGeneratedSupport(
+ output,
+ enumSpecs: enumSpecs,
+ structSpecs: structSpecs,
+ generatedApiName: generatedApiName,
+ emitRegistrationHelper: emitRegistrationHelper,
+ );
+ }
return output.toString();
}
@@ -154,18 +240,26 @@ final class ForyGenerator extends Generator {
String generatedApiName,
) {
final inputFileName = buildStep.inputId.pathSegments.last;
- return library.classes.any(
- (element) =>
- element.displayName == generatedApiName &&
+ for (final element in library.classes) {
+ if (element.displayName == generatedApiName &&
element.firstFragment.libraryFragment.source.shortName ==
- inputFileName,
- );
+ inputFileName) {
+ return true;
+ }
+ }
+ return false;
}
- _GeneratedStructSpec _analyzeStruct(ClassElement element) {
+ _StructOptions _structOptions(InterfaceElement element) {
+ final cached = _structOptionsByElement[element];
+ if (cached != null) {
+ return cached;
+ }
final objectAnnotation = _foryStructChecker.firstAnnotationOf(element);
final objectReader = ConstantReader(objectAnnotation);
final evolving = objectReader.peek('evolving')?.boolValue ?? true;
+ final exposePrivateFields =
+ objectReader.peek('exposePrivateFields')?.boolValue ?? false;
final targetReader = objectReader.peek('target');
final annotatedTarget =
targetReader == null || targetReader.isNull
@@ -176,67 +270,233 @@ final class ForyGenerator extends Generator {
constructorReader == null || constructorReader.isNull
? null
: constructorReader.stringValue;
- if (annotatedTarget == null && constructorName != null) {
+ final options = _StructOptions(
+ evolving: evolving,
+ target: annotatedTarget,
+ constructorName: constructorName,
+ exposePrivateFields: exposePrivateFields,
+ );
+ _structOptionsByElement[element] = options;
+ return options;
+ }
+
+ bool _ownsOrdinarySerializer(ClassElement element) =>
+ !element.isAbstract && element.isConstructable;
+
+ void _validateProviderOnly(InterfaceElement element, _StructOptions options) {
+ if (options.target != null) {
+ throw InvalidGenerationSourceError(
+ 'ForyStruct.target is valid only on an external serializer '
+ 'declaration class.',
+ element: element,
+ );
+ }
+ if (!options.exposePrivateFields) {
throw InvalidGenerationSourceError(
- 'ForyStruct.constructor is valid only when ForyStruct.target is set.',
+ '${element.displayName} does not own a concrete generated serializer. '
+ 'Annotate it with ForyStruct(exposePrivateFields: true) only when it '
+ 'is a public hierarchy boundary that exposes private state.',
element: element,
);
}
+ if (!options.evolving) {
+ throw InvalidGenerationSourceError(
+ 'ForyStruct.evolving has no meaning on provider-only declaration '
+ '${element.displayName}. Remove evolving: false.',
+ element: element,
+ );
+ }
+ if (options.constructorName != null) {
+ throw InvalidGenerationSourceError(
+ 'ForyStruct.constructor has no meaning on provider-only declaration '
+ '${element.displayName}.',
+ element: element,
+ );
+ }
+ }
+
+ _GeneratedStructSpec _analyzeExternalStruct(
+ ClassElement element,
+ _StructOptions options,
+ DartType annotatedTarget,
+ ) {
+ final constructorName = options.constructorName;
final declarationFields = element.fields
.where(_isStoredInstanceField)
.toList(growable: false);
- final targetType =
- annotatedTarget == null
- ? element.thisType
- : _validateExternalTarget(element, annotatedTarget);
- if (annotatedTarget != null) {
- _validateExternalDeclaration(element, targetType, declarationFields);
- }
- final targetTypeLiteral =
- annotatedTarget == null
- ? _typeReferenceLiteral(targetType)
- : _externalTargetTypeLiteral(targetType, element);
+ final targetType = _validateExternalTarget(element, annotatedTarget);
+ _validateExternalDeclaration(element, targetType, declarationFields);
+ final targetTypeLiteral = _externalTargetTypeLiteral(targetType, element);
final selectedConstructor = _selectConstructor(
element,
targetType,
targetTypeLiteral,
constructorName,
- external: annotatedTarget != null,
+ external: true,
);
- final fields = declarationFields
- .where((field) => !_isIgnored(field))
- .map(
- (field) =>
- _analyzeField(element, targetType, targetTypeLiteral, field),
- )
- .toList(growable: false);
- final graphFieldCount =
- annotatedTarget == null
- ? declarationFields.length
- : _externalGraphFieldCount(element, targetType, declarationFields);
-
- final sortedFields = _sortFields(fields);
- final constructionModel = _buildConstructionModel(
+ final indexedCodegenNames = _hasCodegenNameConflict(
+ declarationFields
+ .where((field) => !_isIgnored(field))
+ .map((field) => field.displayName),
+ );
+ final fields = <_GeneratedFieldSpec>[];
+ for (var index = 0; index < declarationFields.length; index += 1) {
+ final field = declarationFields[index];
+ if (_isIgnored(field)) {
+ continue;
+ }
+ fields.add(
+ _analyzeField(
+ element,
+ targetType,
+ targetTypeLiteral,
+ field,
+ indexedCodegenNames ? 'field$index' : field.displayName,
+ ),
+ );
+ }
+ final analyzedConstruction = _buildExternalConstructionModel(
element,
- targetType,
targetTypeLiteral,
selectedConstructor,
constructorName,
- sortedFields,
+ fields,
+ );
+ _validateConstructorSelfReference(
+ declaration: element,
+ targetType: targetType,
+ targetTypeLiteral: targetTypeLiteral,
+ fields: fields,
+ constructionModel: analyzedConstruction,
);
+ _validateWireIdentities(element, fields);
+ final sortedFields = _sortFields(fields);
return _GeneratedStructSpec(
name: element.displayName,
targetType: targetType,
targetTypeLiteral: targetTypeLiteral,
- evolving: evolving,
+ evolving: options.evolving,
fields: sortedFields,
- graphFieldCount: graphFieldCount,
- constructionModel: constructionModel,
+ storageFieldCount: _externalGraphFieldCount(
+ element,
+ targetType,
+ declarationFields,
+ ),
+ constructionModel: analyzedConstruction,
);
}
+ Future<_GeneratedStructSpec> _analyzeOrdinaryStruct(
+ ClassElement element,
+ _StructOptions options,
+ _OrdinaryConstructorAnalyzer constructorAnalyzer,
+ BuildStep buildStep,
+ ) async {
+ if (options.constructorName != null) {
+ throw InvalidGenerationSourceError(
+ 'ForyStruct.constructor is valid only when ForyStruct.target is set. '
+ 'Ordinary structs use their unnamed generative constructor.',
+ element: element,
+ );
+ }
+ final targetType = element.thisType;
+ final targetTypeLiteral = _typeReferenceLiteral(targetType);
+ final selectedConstructor = _selectConstructor(
+ element,
+ targetType,
+ targetTypeLiteral,
+ null,
+ external: false,
+ );
+ final hierarchy = _discoverHierarchyStorage(targetType, element);
+ final discovered = hierarchy.fields;
+ final included = <_DiscoveredField>[];
+ final includedNames = <String>[];
+ for (final field in discovered) {
+ if (_isIgnored(field.declaration)) {
+ continue;
+ }
+ included.add(field);
+ includedNames.add(field.declaration.displayName);
+ }
+ final indexedCodegenNames = _hasCodegenNameConflict(includedNames);
+ final fields = <_GeneratedFieldSpec>[];
+ for (final field in included) {
+ final fieldLibrary = field.declaration.library;
+ if (field.declaration.isPrivate &&
+ fieldLibrary != element.library &&
+ fieldLibrary.uri.scheme == 'file' &&
+ _canonicalLibraryUris?.containsKey(fieldLibrary) != true) {
+ try {
+ final assetId = await buildStep.resolver.assetIdForElement(
+ fieldLibrary,
+ );
+ final canonicalUris =
+ _canonicalLibraryUris ??= Map<LibraryElement, String>.identity();
+ canonicalUris[fieldLibrary] = assetId.uri.toString();
+ } on Object catch (error) {
+ throw InvalidGenerationSourceError(
+ 'Fory cannot derive a stable private-field companion identity '
+ 'from library ${fieldLibrary.uri}: $error',
+ element: field.declaration,
+ );
+ }
+ }
+ final name = field.declaration.displayName;
+ fields.add(
+ _analyzeOrdinaryField(
+ element,
+ targetType,
+ targetTypeLiteral,
+ hierarchy,
+ field,
+ indexedCodegenNames ? 'field${field.storageIndex}' : name,
+ ),
+ );
+ }
+ final analyzedConstruction = await constructorAnalyzer.build(
+ declaration: element,
+ targetTypeLiteral: targetTypeLiteral,
+ constructor: selectedConstructor,
+ fields: fields,
+ );
+ _validateConstructorSelfReference(
+ declaration: element,
+ targetType: targetType,
+ targetTypeLiteral: targetTypeLiteral,
+ fields: fields,
+ constructionModel: analyzedConstruction,
+ );
+ _validateWireIdentities(element, fields);
+ final sortedFields = _sortFields(fields);
+ return _GeneratedStructSpec(
+ name: element.displayName,
+ targetType: targetType,
+ targetTypeLiteral: targetTypeLiteral,
+ evolving: options.evolving,
+ fields: sortedFields,
+ storageFieldCount: hierarchy.fields.length,
+ constructionModel: analyzedConstruction,
+ );
+ }
+
+ bool _hasCodegenNameConflict(Iterable<String> names) {
+ final sourceNames = <String>{};
+ final capitalizedNames = <String>{};
+ for (final name in names) {
+ if (!sourceNames.add(name)) {
+ return true;
+ }
+ final capitalized = '${name[0].toUpperCase()}${name.substring(1)}';
+ if (!capitalizedNames.add(capitalized)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
InterfaceType _validateExternalTarget(
ClassElement declaration,
DartType annotatedTarget,
@@ -422,7 +682,7 @@ final class ForyGenerator extends Generator {
? ''
: '<${alias.typeArguments.map(render).join(', ')}>';
final nullable =
- alias.nullabilitySuffix == NullabilitySuffix.question ? '?' : '';
+ type.nullabilitySuffix == NullabilitySuffix.question ? '?' : '';
return '$base$arguments$nullable';
}
if (type is DynamicType) {
@@ -465,8 +725,8 @@ final class ForyGenerator extends Generator {
typeElement.library?.isDartCore == true) {
return typeElement.displayName;
}
- if (_importPrefixByElement.containsKey(typeElement)) {
- final prefix = _importPrefixByElement[typeElement];
+ if (_hasImportReference(typeElement)) {
+ final prefix = _importPrefixFor(typeElement);
return prefix == null
? typeElement.displayName
: '$prefix.${typeElement.displayName}';
@@ -479,29 +739,1108 @@ final class ForyGenerator extends Generator {
);
}
- void _buildImportPrefixMap(LibraryReader library) {
- _importPrefixByElement.clear();
- for (final import in library.element.firstFragment.libraryImports) {
- final importedLibrary = import.importedLibrary;
- final prefix = import.prefix?.element.displayName;
- if (importedLibrary == null) {
+ bool _hasImportReference(Element element) {
+ final baseElement = element.baseElement;
+ if (!_resolvedImportElements.add(baseElement)) {
+ return _importPrefixByElement.containsKey(baseElement);
+ }
+ final name = element.displayName;
+ final sourceFragment = _sourceLibrary.firstFragment;
+ final unprefixed = sourceFragment.scope.lookup(name).getter;
+ if (unprefixed != null && identical(unprefixed.baseElement, baseElement)) {
+ _importPrefixByElement[baseElement] = null;
+ return true;
+ }
+
+ String? selectedPrefix;
+ for (final import in sourceFragment.libraryImports) {
+ if (import.prefix?.isDeferred == true) {
+ continue;
+ }
+ final prefix = import.prefix?.element;
+ if (prefix == null || prefix.displayName.isEmpty) {
+ continue;
+ }
+ final importedElement = import.namespace.definedNames2[name];
+ if (importedElement == null ||
+ !identical(importedElement.baseElement, baseElement)) {
+ continue;
+ }
+ final resolved = prefix.scope.lookup(name).getter;
+ if (resolved == null || !identical(resolved.baseElement, baseElement)) {
+ continue;
+ }
+ final prefixName = prefix.displayName;
+ if (selectedPrefix == null || prefixName.compareTo(selectedPrefix) < 0) {
+ selectedPrefix = prefixName;
+ }
+ }
+ if (selectedPrefix == null) {
+ return false;
+ }
+ _importPrefixByElement[baseElement] = selectedPrefix;
+ return true;
+ }
+
+ String? _importPrefixFor(Element element) {
+ final library = element.library;
+ if (library == _sourceLibrary || library?.isDartCore == true) {
+ return null;
+ }
+ _hasImportReference(element);
+ return _importPrefixByElement[element.baseElement];
+ }
+
+ _GeneratedEnumSpec _analyzeEnum(EnumElement element) {
+ return _GeneratedEnumSpec(
+ name: element.displayName,
+ usesRawValue: _enumUsesRawValueElement(element),
+ );
+ }
+
+ _HierarchyStorage _discoverHierarchyStorage(
+ InterfaceType targetType,
+ Element errorElement,
+ ) {
+ final cached = _hierarchyStorageByType[targetType];
+ if (cached != null) {
+ return cached;
+ }
+ final layers = <_HierarchyLayer>[];
+ final fields = <_DiscoveredField>[];
+ final visiting = <InterfaceElement>{};
+
+ void appendLayer(InterfaceType type) {
+ if (type.element is ClassElement &&
+ (type.element as ClassElement).isDartCoreObject) {
+ return;
+ }
+ final layer = _HierarchyLayer(type: type, index: layers.length);
+ layers.add(layer);
+ for (final declaration in type.element.fields) {
+ if (!_isStoredInstanceField(declaration)) {
+ continue;
+ }
+ fields.add(
+ _DiscoveredField(
+ declaration: declaration.baseElement,
+ layer: layer,
+ storageIndex: fields.length,
+ ),
+ );
+ }
+ }
+
+ void visitClass(InterfaceType type) {
+ final element = type.element;
+ if (element is ClassElement && element.isDartCoreObject) {
+ return;
+ }
+ if (!visiting.add(element)) {
+ throw InvalidGenerationSourceError(
+ 'Fory cannot flatten a cyclic superclass or mixin storage chain for '
+ '${targetType.getDisplayString()}.',
+ element: errorElement,
+ );
+ }
+ final superclass = type.superclass;
+ if (superclass != null) {
+ visitClass(superclass);
+ }
+ for (final mixin in type.mixins) {
+ appendLayer(mixin);
+ }
+ appendLayer(type);
+ visiting.remove(element);
+ }
+
+ if (targetType.element is MixinElement) {
+ appendLayer(targetType);
+ } else {
+ visitClass(targetType);
+ }
+ final hierarchy = _HierarchyStorage(layers: layers, fields: fields);
+ _hierarchyStorageByType[targetType] = hierarchy;
+ return hierarchy;
+ }
+
+ _GeneratedFieldSpec _analyzeOrdinaryField(
+ ClassElement declaration,
+ InterfaceType targetType,
+ String targetTypeLiteral,
+ _HierarchyStorage hierarchy,
+ _DiscoveredField discovered,
+ String codegenName,
+ ) {
+ final field = discovered.declaration;
+ final effectiveType = _effectiveFieldType(discovered);
+ _validateOrdinaryFieldType(effectiveType, field, targetTypeLiteral);
+ _validateTypeVisibleFrom(
+ effectiveType,
+ declaration.library,
+ errorElement: field,
+ context:
+ 'Storage field ${field.enclosingElement.displayName}.'
+ '${field.displayName}',
+ );
+ final access = _resolveOrdinaryFieldAccess(
+ declaration,
+ targetType,
+ hierarchy,
+ discovered,
+ effectiveType,
+ );
+ return _createGeneratedFieldSpec(
+ annotationField: field,
+ effectiveType: effectiveType,
+ codegenName: codegenName,
+ writable: !field.isFinal,
+ access: access,
+ );
+ }
+
+ void _validateOrdinaryFieldType(
+ DartType type,
+ FieldElement field,
+ String targetTypeLiteral,
+ ) {
+ void validate(DartType current) {
+ if (current is DynamicType) {
+ return;
+ }
+ if (current is InvalidType || current is TypeParameterType) {
+ throw InvalidGenerationSourceError(
+ 'Storage field ${field.enclosingElement.displayName}.'
+ '${field.displayName} has unresolved effective type '
+ '${type.getDisplayString()} while generating $targetTypeLiteral.',
+ element: field,
+ );
+ }
+ if (current is FunctionType ||
+ current is RecordType ||
+ current is VoidType ||
+ current is NeverType ||
+ current.isDartCoreFunction ||
+ current.isDartCoreRecord ||
+ current.isDartCoreNull) {
+ throw InvalidGenerationSourceError(
+ 'Storage field ${field.enclosingElement.displayName}.'
+ '${field.displayName} has unsupported effective type '
+ '${type.getDisplayString()}. Mark the field with '
+ '@ForyField(ignore: true), use a supported serializable type, or '
+ 'use a manual serializer.',
+ element: field,
+ );
+ }
+ if (current is InterfaceType) {
+ for (final argument in current.typeArguments) {
+ validate(argument);
+ }
+ return;
+ }
+ throw InvalidGenerationSourceError(
+ 'Storage field ${field.enclosingElement.displayName}.'
+ '${field.displayName} has unsupported effective type '
+ '${type.getDisplayString()}. Mark the field with '
+ '@ForyField(ignore: true), use a supported serializable type, or use '
+ 'a manual serializer.',
+ element: field,
+ );
+ }
+
+ validate(type);
+ }
+
+ DartType _effectiveFieldType(_DiscoveredField discovered) {
+ final declaration = discovered.declaration;
+ final getter = discovered.layer.type.getGetter(declaration.displayName);
+ final variable = getter?.variable;
+ if (getter == null ||
+ variable is! FieldElement ||
+ !identical(variable.baseElement, declaration.baseElement)) {
+ throw InvalidGenerationSourceError(
+ 'Fory could not substitute storage field '
+ '${discovered.layer.type.element.displayName}.'
+ '${declaration.displayName} in its instantiated hierarchy layer.',
+ element: declaration,
+ );
+ }
+ return getter.returnType;
+ }
+
+ _FieldAccessPlan _resolveOrdinaryFieldAccess(
+ ClassElement child,
+ InterfaceType targetType,
+ _HierarchyStorage hierarchy,
+ _DiscoveredField discovered,
+ DartType effectiveType,
+ ) {
+ final field = discovered.declaration;
+ final sameLibrary = field.library == child.library;
+ if (field.isPublic || sameLibrary) {
+ _validateExactFieldAccess(
+ targetType: targetType,
+ lookupLibrary: child.library,
+ discovered: discovered,
+ effectiveType: effectiveType,
+ hierarchy: hierarchy,
+ requireSetter: !field.isFinal,
+ child: child,
+ );
+ return _FieldAccessPlan.direct(field.displayName);
+ }
+
+ final boundary = _resolvePrivateAccessBoundary(
+ child: child,
+ hierarchy: hierarchy,
+ discovered: discovered,
+ );
+ _validateExactFieldAccess(
+ targetType: targetType,
+ lookupLibrary: field.library,
+ discovered: discovered,
+ effectiveType: effectiveType,
+ hierarchy: hierarchy,
+ requireSetter: !field.isFinal,
+ child: child,
+ );
+ final identity = _privateFieldIdentity(field);
+ final digest = _privateFieldDigest(identity);
+ return _FieldAccessPlan.companion(
+ fieldName: field.displayName,
+ companion: boundary,
+ getter: '\$g$digest',
+ setter: field.isFinal ? null : '\$s$digest',
+ );
+ }
+
+ String _resolvePrivateAccessBoundary({
+ required ClassElement child,
+ required _HierarchyStorage hierarchy,
+ required _DiscoveredField discovered,
+ }) {
+ final field = discovered.declaration;
+ final authorizedHelpers = <String>{};
+ for (final layer in hierarchy.layers.reversed) {
+ if (layer.index < discovered.layer.index) {
+ continue;
+ }
+ final boundary = layer.type.element;
+ if (boundary.library != field.library ||
+ boundary.isPrivate ||
+ !_foryStructChecker.hasAnnotationOf(boundary)) {
continue;
}
+ final options = _structOptions(boundary);
+ if (!options.exposePrivateFields || options.target != null) {
+ continue;
+ }
+ final boundaryHierarchy = _discoverHierarchyStorage(
+ boundary.thisType,
+ boundary,
+ );
+ var carriesField = false;
+ for (final candidate in boundaryHierarchy.fields) {
+ if (identical(candidate.declaration.baseElement, field.baseElement)) {
+ carriesField = true;
+ break;
+ }
+ }
+ if (!carriesField) {
+ continue;
+ }
+ final helperName = _privateAccessHelperName(boundary);
+ authorizedHelpers.add(helperName);
+ final reference = _companionReferenceFromChild(
+ child.library,
+ boundary,
+ helperName,
+ );
+ if (reference != null) {
+ return reference;
+ }
+ }
+
+ final helperNames = authorizedHelpers.toList()..sort();
+ final authorization =
+ helperNames.isNotEmpty
+ ? 'An authorized public boundary exists, but its generated '
+ 'companion is not visible through the child library imports '
+ 'and re-exports. Expected ${helperNames.join(' or ')}.'
+ : 'No public hierarchy boundary in the declaring library is '
+ 'annotated with '
+ '@ForyStruct(exposePrivateFields: true).';
+ final consumerPermission =
+ _structOptions(child).exposePrivateFields
+ ? ' The annotation on ${child.displayName} authorizes only '
+ 'private storage declared by ${child.library.uri}; it cannot '
+ 'authorize this field.'
+ : '';
+ throw InvalidGenerationSourceError(
+ 'Fory discovered inherited private field '
+ '${field.enclosingElement.displayName}.${field.displayName}, declared '
+ 'by ${field.library.uri}, while generating ${child.displayName} in '
+ '${child.library.uri}. $authorization$consumerPermission Import a '
+ 'qualifying boundary together with its generated companion, mark the '
+ 'field with @ForyField(ignore: true), or use a manual serializer.',
+ element: field,
+ );
+ }
+
+ String? _companionReferenceFromChild(
+ LibraryElement childLibrary,
+ InterfaceElement boundary,
+ String helperName,
+ ) {
+ Element? localCollision = childLibrary.getClass(helperName);
+ localCollision ??= childLibrary.getMixin(helperName);
+ localCollision ??= childLibrary.getEnum(helperName);
+ localCollision ??= childLibrary.getExtension(helperName);
+ localCollision ??= childLibrary.getExtensionType(helperName);
+ localCollision ??= childLibrary.getTypeAlias(helperName);
+ localCollision ??= childLibrary.getTopLevelFunction(helperName);
+ localCollision ??= childLibrary.getTopLevelVariable(helperName);
+ localCollision ??= childLibrary.getGetter(helperName);
+ localCollision ??= childLibrary.getSetter(helperName);
+
+ final validPrefixes = <String?>{};
+ final ambiguousPrefixes = <String?>{};
+ var blockedByLocalCollision = false;
+ final importsByPrefix = <String?, List<LibraryImport>>{};
+ for (final import in childLibrary.firstFragment.libraryImports) {
+ final prefix = import.prefix?.element.displayName;
final normalizedPrefix = prefix == null || prefix.isEmpty ? null : prefix;
- for (final importedElement in import.namespace.definedNames2.values) {
- if (!_importPrefixByElement.containsKey(importedElement) ||
- normalizedPrefix == null) {
- _importPrefixByElement[importedElement] = normalizedPrefix;
+ if (normalizedPrefix == helperName) {
+ localCollision ??= import.prefix!.element;
+ }
+ if (import.prefix?.isDeferred == true) {
+ continue;
+ }
+ var imports = importsByPrefix[normalizedPrefix];
+ if (imports == null) {
+ imports = <LibraryImport>[];
+ importsByPrefix[normalizedPrefix] = imports;
+ }
+ imports.add(import);
+ }
+ for (final MapEntry(key: prefix, value: imports)
+ in importsByPrefix.entries) {
+ final resolvedBoundary =
+ prefix == null
+ ? childLibrary.firstFragment.scope
+ .lookup(boundary.displayName)
+ .getter
+ : imports.first.prefix!.element.scope
+ .lookup(boundary.displayName)
+ .getter;
+ if (resolvedBoundary == null ||
+ !identical(resolvedBoundary.baseElement, boundary.baseElement)) {
+ continue;
+ }
+ final helperOwners = <LibraryElement>{};
+ for (final import in imports) {
+ final importedLibrary = import.importedLibrary;
+ if (importedLibrary == null) {
+ continue;
+ }
+ if (!_combinatorsAllow(import.combinators, helperName)) {
+ continue;
+ }
+ helperOwners.addAll(
+ _companionOwners(importedLibrary, helperName, <LibraryElement>{}),
+ );
+ }
+ if (prefix == null &&
+ localCollision != null &&
+ localCollision.library != boundary.library &&
+ helperOwners.contains(boundary.library)) {
+ blockedByLocalCollision = true;
+ continue;
+ }
+ if (helperOwners.contains(boundary.library) && helperOwners.length > 1) {
+ ambiguousPrefixes.add(prefix);
+ continue;
+ }
+ if (helperOwners.length == 1 && helperOwners.single == boundary.library) {
+ validPrefixes.add(prefix);
+ }
+ }
+ if (validPrefixes.isEmpty) {
+ if (blockedByLocalCollision) {
+ throw InvalidGenerationSourceError(
+ 'Generated companion name $helperName from ${boundary.library.uri} '
+ 'collides with ${localCollision!.displayName} in '
+ '${childLibrary.uri}.',
+ element: boundary,
+ );
+ }
+ if (ambiguousPrefixes.isNotEmpty) {
+ final names = <String>[];
+ for (final prefix in ambiguousPrefixes) {
+ names.add(prefix ?? '<unprefixed>');
+ }
+ names.sort();
+ throw InvalidGenerationSourceError(
+ 'Generated companion $helperName is ambiguous in import namespace '
+ '${names.join(', ')}.',
+ element: boundary,
+ );
+ }
+ return null;
+ }
+ if (validPrefixes.contains(null)) {
+ return helperName;
+ }
+ String? prefix;
+ for (final candidate in validPrefixes) {
+ if (candidate != null &&
+ (prefix == null || candidate.compareTo(prefix) < 0)) {
+ prefix = candidate;
+ }
+ }
+ return prefix == null ? helperName : '$prefix.$helperName';
+ }
+
+ Set<LibraryElement> _companionOwners(
+ LibraryElement current,
+ String name,
+ Set<LibraryElement> visiting,
+ ) {
+ if (!visiting.add(current)) {
+ return const <LibraryElement>{};
+ }
+ final owners = <LibraryElement>{};
+ if (_generatesCompanion(current, name)) {
+ visiting.remove(current);
+ return <LibraryElement>{current};
+ }
+ final existing = current.publicNamespace.definedNames2[name];
+ final existingLibrary = existing?.library;
+ if (existingLibrary == current) {
+ visiting.remove(current);
+ return <LibraryElement>{current};
+ }
+ if (existingLibrary != null) {
+ owners.add(existingLibrary);
+ }
+ for (final export in current.firstFragment.libraryExports) {
+ final exported = export.exportedLibrary;
+ if (exported == null || !_combinatorsAllow(export.combinators, name)) {
+ continue;
+ }
+ owners.addAll(_companionOwners(exported, name, visiting));
+ }
+ visiting.remove(current);
+ return owners;
+ }
+
+ bool _generatesCompanion(LibraryElement library, String helperName) {
+ const suffix = 'ForyFieldAccess';
+ if (!helperName.startsWith(r'$') || !helperName.endsWith(suffix)) {
+ return false;
+ }
+ final boundaryName = helperName.substring(
+ 1,
+ helperName.length - suffix.length,
+ );
+ final boundary =
+ library.getClass(boundaryName) ?? library.getMixin(boundaryName);
+ if (boundary == null ||
+ boundary.isPrivate ||
+ !_foryStructChecker.hasAnnotationOf(boundary)) {
+ return false;
+ }
+ final options = _structOptions(boundary);
+ return options.target == null && options.exposePrivateFields;
+ }
+
+ bool _combinatorsAllow(List<NamespaceCombinator> combinators, String name) {
+ var visible = true;
+ for (final combinator in combinators) {
+ if (combinator is ShowElementCombinator) {
+ visible = visible && combinator.shownNames.contains(name);
+ } else if (combinator is HideElementCombinator) {
+ visible = visible && !combinator.hiddenNames.contains(name);
+ }
+ }
+ return visible;
+ }
+
+ Future<_PrivateAccessCompanionSpec> _analyzePrivateAccessCompanion(
+ InterfaceElement boundary,
+ BuildStep buildStep,
+ ) async {
+ if (boundary.isPrivate) {
+ throw InvalidGenerationSourceError(
+ 'ForyStruct.exposePrivateFields requires a public hierarchy boundary; '
+ '${boundary.displayName} is private.',
+ element: boundary,
+ );
+ }
+ if (boundary is ClassElement &&
+ !boundary.isExtendableOutside &&
+ !boundary.isMixableOutside) {
+ throw InvalidGenerationSourceError(
+ '${boundary.displayName} cannot be extended or mixed in outside '
+ '${boundary.library.uri}, so it cannot expose inherited private '
+ 'state to consumer subclasses.',
+ element: boundary,
+ );
+ }
+ final boundaryType = boundary.thisType;
+ final hierarchy = _discoverHierarchyStorage(boundaryType, boundary);
+ final fields = <_DiscoveredField>[];
+ for (final field in hierarchy.fields) {
+ if (field.declaration.isPrivate &&
+ field.declaration.library == boundary.library &&
+ !_isIgnored(field.declaration)) {
+ fields.add(field);
+ }
+ }
+ if (fields.isEmpty) {
+ throw InvalidGenerationSourceError(
+ 'ForyStruct.exposePrivateFields on ${boundary.displayName} has no '
+ 'non-ignored private storage owned by ${boundary.library.uri} to '
+ 'expose.',
+ element: boundary,
+ );
+ }
+
+ final helperName = _privateAccessHelperName(boundary);
+ final existing = boundary.library.publicNamespace.definedNames2[helperName];
+ if (existing != null) {
+ final existingAsset = await buildStep.resolver.assetIdForElement(
+ existing,
+ );
+ if (!buildStep.allowedOutputs.contains(existingAsset)) {
+ throw InvalidGenerationSourceError(
+ 'Generated companion name $helperName collides with an existing '
+ 'top-level declaration in ${boundary.library.uri}.',
+ element: boundary,
+ );
+ }
+ }
+
+ final typeParameters = boundary.typeParameters.toSet();
+ final receiverType = _companionBoundaryType(boundary);
+ final methodTypeParameters = _companionMethodTypeParameters(boundary);
+ if (boundary.library != _sourceLibrary) {
+ throw StateError(
+ 'Private-field companion boundary ${boundary.displayName} is not '
+ 'owned by the current input library.',
+ );
+ }
+ if (boundary.library.uri.scheme == 'file') {
+ final canonicalUris =
+ _canonicalLibraryUris ??= Map<LibraryElement, String>.identity();
+ canonicalUris[boundary.library] = buildStep.inputId.uri.toString();
+ }
+ final methods = <_PrivateAccessMethodSpec>[];
+ final identitiesByDigest = <String, String>{};
+ for (final discovered in fields) {
+ final field = discovered.declaration;
+ final effectiveType = _effectiveFieldType(discovered);
+ _validateExactFieldAccess(
+ targetType: boundaryType,
+ lookupLibrary: boundary.library,
+ discovered: discovered,
+ effectiveType: effectiveType,
+ hierarchy: hierarchy,
+ requireSetter: !field.isFinal,
+ child: boundary,
+ );
+ final identity = _privateFieldIdentity(field);
+ final digest = _privateFieldDigest(identity);
+ final previous = identitiesByDigest[digest];
+ if (previous != null && previous != identity) {
+ throw InvalidGenerationSourceError(
+ 'Private-field access digest collision between $previous and '
+ '$identity.',
+ element: field,
+ );
+ }
+ identitiesByDigest[digest] = identity;
+ methods.add(
+ _PrivateAccessMethodSpec(
+ fieldName: field.displayName,
+ fieldType: _companionTypeCode(
+ effectiveType,
+ boundary,
+ allowedTypeParameters: typeParameters,
+ ),
+ getterName: '\$g$digest',
+ setterName: field.isFinal ? null : '\$s$digest',
+ ),
+ );
+ }
+ methods.sort((left, right) => left.getterName.compareTo(right.getterName));
+ return _PrivateAccessCompanionSpec(
+ name: helperName,
+ receiverType: receiverType,
+ methodTypeParameters: methodTypeParameters,
+ methods: methods,
+ );
+ }
+
+ String _companionBoundaryType(InterfaceElement boundary) {
+ if (boundary.typeParameters.isEmpty) {
+ return boundary.displayName;
+ }
+ final arguments = <String>[];
+ for (final parameter in boundary.typeParameters) {
+ arguments.add(parameter.displayName);
+ }
+ return '${boundary.displayName}<${arguments.join(', ')}>';
+ }
+
+ String _companionMethodTypeParameters(InterfaceElement boundary) {
+ if (boundary.typeParameters.isEmpty) {
+ return '';
+ }
+ final allowed = boundary.typeParameters.toSet();
+ final parameters = <String>[];
+ for (final parameter in boundary.typeParameters) {
+ final bound = parameter.bound;
+ parameters.add(
+ bound == null
+ ? parameter.displayName
+ : '${parameter.displayName} extends '
+ '${_companionTypeCode(bound, boundary, allowedTypeParameters: allowed)}',
+ );
+ }
+ return '<${parameters.join(', ')}>';
+ }
+
+ String _companionTypeCode(
+ DartType type,
+ InterfaceElement boundary, {
+ required Set<TypeParameterElement> allowedTypeParameters,
+ bool requireNamespace = true,
+ bool useAlias = true,
+ }) {
+ if (type is DynamicType ||
+ type is FunctionType ||
+ type is InvalidType ||
+ type.isDartCoreFunction) {
+ throw InvalidGenerationSourceError(
+ 'Private-field companion for ${boundary.displayName} cannot expose '
+ '${type.getDisplayString()}. Companion signatures must be exact, '
+ 'public, and non-callback.',
+ element: boundary,
+ );
+ }
+ if (type is RecordType || type.isDartCoreRecord || type.isDartCoreNull) {
+ throw InvalidGenerationSourceError(
+ 'Private-field companion for ${boundary.displayName} cannot expose '
+ '${type.getDisplayString()} because ordinary ForyStruct fields do '
+ 'not support that type.',
+ element: boundary,
+ );
+ }
+ final nullable =
+ type.nullabilitySuffix == NullabilitySuffix.question ? '?' : '';
+ final alias = type.alias;
+ if (useAlias && alias != null && alias.element.isPublic) {
+ final base = _companionTypeName(
+ alias.element,
+ boundary,
+ requireNamespace: requireNamespace,
+ );
+ final arguments = _companionTypeArguments(
+ alias.typeArguments,
+ boundary,
+ allowedTypeParameters: allowedTypeParameters,
+ requireNamespace: requireNamespace,
+ useAlias: true,
+ );
+ // A public alias supplies the emitted name, but it cannot conceal a
+ // private nominal type or callback in its underlying signature.
+ _companionTypeCode(
+ type,
+ boundary,
+ allowedTypeParameters: allowedTypeParameters,
+ requireNamespace: false,
+ useAlias: false,
+ );
+ return arguments.isEmpty
+ ? '$base$nullable'
+ : '$base<$arguments>$nullable';
+ }
+ if (type is InterfaceType) {
+ final element = type.element;
+ final base = _companionTypeName(
+ element,
+ boundary,
+ requireNamespace: requireNamespace,
+ );
+ final arguments = _companionTypeArguments(
+ type.typeArguments,
+ boundary,
+ allowedTypeParameters: allowedTypeParameters,
+ requireNamespace: requireNamespace,
+ useAlias: useAlias,
+ );
+ return arguments.isEmpty
+ ? '$base$nullable'
+ : '$base<$arguments>$nullable';
+ }
+ if (type is TypeParameterType) {
+ if (!allowedTypeParameters.contains(type.element)) {
+ throw InvalidGenerationSourceError(
+ 'Private-field companion for ${boundary.displayName} contains '
+ 'unbound type parameter ${type.element.displayName}.',
+ element: boundary,
+ );
+ }
+ return '${type.element.displayName}$nullable';
+ }
+ throw InvalidGenerationSourceError(
+ 'Private-field companion for ${boundary.displayName} cannot render '
+ '${type.getDisplayString()} as an exact public Dart type.',
+ element: boundary,
+ );
+ }
+
+ String _companionTypeArguments(
+ List<DartType> arguments,
+ InterfaceElement boundary, {
+ required Set<TypeParameterElement> allowedTypeParameters,
+ required bool requireNamespace,
+ required bool useAlias,
+ }) {
+ if (arguments.isEmpty) {
+ return '';
+ }
+ final rendered = <String>[];
+ for (final argument in arguments) {
+ rendered.add(
+ _companionTypeCode(
+ argument,
+ boundary,
+ allowedTypeParameters: allowedTypeParameters,
+ requireNamespace: requireNamespace,
+ useAlias: useAlias,
+ ),
+ );
+ }
+ return rendered.join(', ');
+ }
+
+ String _companionTypeName(
+ Element element,
+ InterfaceElement boundary, {
+ required bool requireNamespace,
+ }) {
+ if (element.isPrivate) {
+ throw InvalidGenerationSourceError(
+ 'Private-field companion for ${boundary.displayName} would expose '
+ 'private nominal type ${element.displayName}.',
+ element: boundary,
+ );
+ }
+ final library = element.library;
+ if (library != null &&
+ library != boundary.library &&
+ !library.isDartCore &&
+ requireNamespace &&
+ !_hasImportReference(element)) {
+ throw InvalidGenerationSourceError(
+ 'Private-field companion for ${boundary.displayName} uses public type '
+ '${element.displayName}, but that exact type is not nameable from '
+ '${boundary.library.uri}.',
+ element: boundary,
+ );
+ }
+ if (element.library == boundary.library ||
+ element.library?.isDartCore == true) {
+ return element.displayName;
+ }
+ final prefix = _importPrefixFor(element);
+ return prefix == null
+ ? element.displayName
+ : '$prefix.${element.displayName}';
+ }
+
+ String _privateAccessHelperName(InterfaceElement boundary) =>
+ '\$${boundary.displayName}ForyFieldAccess';
+
+ String _privateFieldIdentity(FieldElement field) =>
+ '${_canonicalLibraryUri(field.library)}\u0000'
+ '${field.enclosingElement.displayName}\u0000${field.displayName}';
+
+ String _privateFieldDigest(String identity) {
+ var hash = BigInt.parse('cbf29ce484222325', radix: 16);
+ final prime = BigInt.parse('100000001b3', radix: 16);
+ final mask = (BigInt.one << 64) - BigInt.one;
+ for (final byte in utf8.encode(identity)) {
+ hash = ((hash ^ BigInt.from(byte)) * prime) & mask;
+ }
+ return hash.toRadixString(16).padLeft(16, '0');
+ }
+
+ String _canonicalLibraryUri(LibraryElement library) {
+ final uri = library.uri;
+ if (uri.scheme != 'file') {
+ return uri.toString();
+ }
+ final canonical = _canonicalLibraryUris?[library];
+ if (canonical == null) {
+ throw StateError(
+ 'Canonical asset URI was not prepared for ${library.uri}.',
+ );
+ }
+ return canonical;
+ }
+
+ void _writePrivateAccessCompanion(
+ StringBuffer output,
+ _PrivateAccessCompanionSpec companion,
+ ) {
+ output
+ ..writeln('/// @nodoc')
+ ..writeln('abstract final class ${companion.name} {');
+ for (final method in companion.methods) {
+ output
+ ..writeln(" @pragma('vm:prefer-inline')")
+ ..writeln(
+ ' static ${method.fieldType} ${method.getterName}'
+ '${companion.methodTypeParameters}('
+ '${companion.receiverType} value) => value.${method.fieldName};',
+ )
+ ..writeln();
+ final setterName = method.setterName;
+ if (setterName != null) {
+ output
+ ..writeln(" @pragma('vm:prefer-inline')")
+ ..writeln(
+ ' static void $setterName${companion.methodTypeParameters}(',
+ )
+ ..writeln(' ${companion.receiverType} value,')
+ ..writeln(' ${method.fieldType} fieldValue,')
+ ..writeln(' ) {')
+ ..writeln(' value.${method.fieldName} = fieldValue;')
+ ..writeln(' }')
+ ..writeln();
+ }
+ }
+ output
+ ..writeln('}')
+ ..writeln();
+ }
+
+ void _validateExactFieldAccess({
+ required InterfaceType targetType,
+ required LibraryElement lookupLibrary,
+ required _DiscoveredField discovered,
+ required DartType effectiveType,
+ required _HierarchyStorage hierarchy,
+ required bool requireSetter,
+ required InterfaceElement child,
+ }) {
+ final field = discovered.declaration;
+ _DiscoveredField? laterApplication;
+ for (
+ var index = discovered.storageIndex + 1;
+ index < hierarchy.fields.length;
+ index += 1
+ ) {
+ final candidate = hierarchy.fields[index];
+ if (identical(candidate.declaration.baseElement, field.baseElement)) {
+ laterApplication = candidate;
+ break;
+ }
+ }
+ if (laterApplication != null) {
+ throw InvalidGenerationSourceError(
+ 'Fory cannot address storage field '
+ '${field.enclosingElement.displayName}.${field.displayName} in '
+ '${child.displayName}: the same field declaration is applied more '
+ 'than once and this slot is hidden by a later application.',
+ element: field,
+ );
+ }
+
+ final getter = targetType.lookUpGetter(
+ field.displayName,
+ lookupLibrary,
+ concrete: true,
+ );
+ final getterVariable = getter?.variable;
+ if (getter == null ||
+ getterVariable is! FieldElement ||
+ !identical(getterVariable.baseElement, field.baseElement) ||
+ getter.returnType != effectiveType) {
+ throw InvalidGenerationSourceError(
+ 'Fory discovered storage field '
+ '${field.enclosingElement.displayName}.${field.displayName}, but '
+ '${child.displayName} does not expose that exact slot through its '
+ 'effective getter. A later field or accessor hides the storage.',
+ element: field,
+ );
+ }
+ if (!requireSetter) {
+ return;
+ }
+ final setter = targetType.lookUpSetter(
+ field.displayName,
+ lookupLibrary,
+ concrete: true,
+ );
+ final setterVariable = setter?.variable;
+ final parameters = setter?.formalParameters;
+ if (setter == null ||
+ setterVariable is! FieldElement ||
+ !identical(setterVariable.baseElement, field.baseElement) ||
+ parameters == null ||
+ parameters.length != 1 ||
+ parameters.single.type != effectiveType) {
+ throw InvalidGenerationSourceError(
+ 'Mutable storage field ${field.enclosingElement.displayName}.'
+ '${field.displayName} on ${child.displayName} does not expose an '
+ 'exact setter for ${_typeCodeString(effectiveType)}.',
+ element: field,
+ );
+ }
+ }
+
+ void _validateTypeVisibleFrom(
+ DartType type,
+ LibraryElement library, {
+ required Element errorElement,
+ required String context,
+ }) {
+ void validateElement(Element element, {required bool requireNamespace}) {
+ final elementLibrary = element.library;
+ if (elementLibrary == null ||
+ elementLibrary == library ||
+ elementLibrary.isDartCore) {
+ return;
+ }
+ if (element.isPrivate ||
+ (requireNamespace && !_hasImportReference(element))) {
+ throw InvalidGenerationSourceError(
+ '$context uses type ${element.displayName}, which is not nameable '
+ 'from ${library.uri}. Import the exact public type or ignore the '
+ 'field explicitly.',
+ element: errorElement,
+ );
+ }
+ }
+
+ void validate(
+ DartType current, {
+ bool requireNamespace = true,
+ bool useAlias = true,
+ }) {
+ if (current is InvalidType) {
+ throw InvalidGenerationSourceError(
+ '$context has an invalid or unresolved Dart type.',
+ element: errorElement,
+ );
+ }
+ final alias = current.alias;
+ if (useAlias && alias != null) {
+ final aliasElement = alias.element;
+ final aliasVisible =
+ aliasElement.library == library ||
+ (aliasElement.isPublic && _hasImportReference(aliasElement));
+ if (aliasVisible) {
+ validateElement(aliasElement, requireNamespace: requireNamespace);
+ for (final argument in alias.typeArguments) {
+ validate(argument);
+ }
+ // The visible alias is sufficient for generated source. Continue
+ // only to reject private nominal types hidden under that alias.
+ validate(current, requireNamespace: false, useAlias: false);
+ return;
+ }
+ }
+ if (current is InterfaceType) {
+ validateElement(current.element, requireNamespace: requireNamespace);
+ for (final argument in current.typeArguments) {
+ validate(
+ argument,
+ requireNamespace: requireNamespace,
+ useAlias: useAlias,
+ );
+ }
+ return;
+ }
+ if (current is TypeParameterType) {
+ throw InvalidGenerationSourceError(
+ '$context contains unresolved type parameter '
+ '${current.element.displayName}.',
+ element: errorElement,
+ );
+ }
+ if (current is FunctionType) {
+ validate(
+ current.returnType,
+ requireNamespace: requireNamespace,
+ useAlias: useAlias,
+ );
+ for (final parameter in current.formalParameters) {
+ validate(
+ parameter.type,
+ requireNamespace: requireNamespace,
+ useAlias: useAlias,
+ );
+ }
+ return;
+ }
+ if (current is RecordType) {
+ for (final field in current.positionalFields) {
+ validate(
+ field.type,
+ requireNamespace: requireNamespace,
+ useAlias: useAlias,
+ );
+ }
+ for (final field in current.namedFields) {
+ validate(
+ field.type,
+ requireNamespace: requireNamespace,
+ useAlias: useAlias,
+ );
}
}
}
+
+ validate(type);
}
- _GeneratedEnumSpec _analyzeEnum(EnumElement element) {
- return _GeneratedEnumSpec(
- name: element.displayName,
- usesRawValue: _enumUsesRawValueElement(element),
- );
+ void _validateWireIdentities(
+ ClassElement declaration,
+ List<_GeneratedFieldSpec> fields,
+ ) {
+ final ids = <int, _GeneratedFieldSpec>{};
+ final names = <String, _GeneratedFieldSpec>{};
+ for (final field in fields) {
+ final id = field.id;
+ if (id != null) {
+ final previous = ids[id];
+ if (previous != null) {
+ throw InvalidGenerationSourceError(
+ 'Fory struct ${declaration.displayName} has duplicate field id '
+ '$id on ${previous.name} and ${field.name}.',
+ element: field.declaration,
+ );
+ }
+ ids[id] = field;
+ } else {
+ final wireName = field.wireName!;
+ final previous = names[wireName];
+ if (previous != null) {
+ throw InvalidGenerationSourceError(
+ 'Fory struct ${declaration.displayName} has duplicate canonical '
+ 'field name $wireName on ${previous.name} and ${field.name}. '
+ 'Assign distinct numeric ids or ignore one field explicitly.',
+ element: field.declaration,
+ );
+ }
+ names[wireName] = field;
+ }
+ }
}
_GeneratedFieldSpec _analyzeField(
@@ -509,31 +1848,8 @@ final class ForyGenerator extends Generator {
InterfaceType targetType,
String targetTypeLiteral,
FieldElement field,
+ String codegenName,
) {
- final annotation = _fieldAnnotationOf(field);
- final reader = annotation == null ? null : ConstantReader(annotation);
- final idValue = reader?.peek('id');
- final nullableValue = reader?.peek('nullable');
- final dynamicValue = reader?.peek('dynamic');
- final rawFieldId =
- idValue == null || idValue.isNull ? null : idValue.intValue;
- if (rawFieldId != null && rawFieldId < 0) {
- throw InvalidGenerationSourceError(
- 'Fory field id must be non-negative.',
- element: field,
- );
- }
- final fieldId = rawFieldId;
- final nullable =
- nullableValue == null || nullableValue.isNull
- ? _isNullable(field.type)
- : nullableValue.boolValue;
- final dynamic =
- dynamicValue == null || dynamicValue.isNull
- ? _autoDynamic(field.type)
- : dynamicValue.boolValue;
- final ref = reader?.peek('ref')?.boolValue ?? false;
- final typeSpec = _analyzeTypeSpecAnnotation(field, reader);
final getter = targetType.lookUpGetter(
field.displayName,
declaration.library,
@@ -580,24 +1896,63 @@ final class ForyGenerator extends Generator {
}
}
- return _GeneratedFieldSpec(
- name: field.displayName,
- type: field.type,
- displayType: _typeCodeString(field.type),
- identifier:
- fieldId != null ? '$fieldId' : _toSnakeCase(field.displayName),
- id: fieldId,
- nullable: nullable,
- ref: ref,
- dynamic: dynamic,
+ return _createGeneratedFieldSpec(
+ annotationField: field,
+ effectiveType: field.type,
+ codegenName: codegenName,
writable: setter != null,
+ access: _FieldAccessPlan.direct(field.displayName),
+ );
+ }
+
+ _GeneratedFieldSpec _createGeneratedFieldSpec({
+ required FieldElement annotationField,
+ required DartType effectiveType,
+ required String codegenName,
+ required bool writable,
+ required _FieldAccessPlan access,
+ }) {
+ final annotation = _fieldAnnotationOf(annotationField);
+ final reader = annotation == null ? null : ConstantReader(annotation);
+ final idValue = reader?.peek('id');
+ final nullableValue = reader?.peek('nullable');
+ final dynamicValue = reader?.peek('dynamic');
+ final rawFieldId =
+ idValue == null || idValue.isNull ? null : idValue.intValue;
+ if (rawFieldId != null && rawFieldId < 0) {
+ throw InvalidGenerationSourceError(
+ 'Fory field id must be non-negative.',
+ element: annotationField,
+ );
+ }
+ final nullable =
+ nullableValue == null || nullableValue.isNull
+ ? _isNullable(effectiveType)
+ : nullableValue.boolValue;
+ final dynamic =
+ dynamicValue == null || dynamicValue.isNull
+ ? _autoDynamic(effectiveType)
+ : dynamicValue.boolValue;
+ final ref = reader?.peek('ref')?.boolValue ?? false;
+ final typeSpec = _analyzeTypeSpecAnnotation(annotationField, reader);
+ return _GeneratedFieldSpec(
+ name: annotationField.displayName,
+ type: effectiveType,
+ displayType: _typeCodeString(effectiveType),
+ wireName:
+ rawFieldId == null ? _toSnakeCase(annotationField.displayName) : null,
+ id: rawFieldId,
+ writable: writable,
+ codegenName: codegenName,
+ declaration: annotationField.baseElement,
+ access: access,
fieldType: _fieldTypeForType(
- field.type,
+ effectiveType,
nullable: nullable,
ref: ref,
dynamic: dynamic,
typeSpec: typeSpec,
- errorElement: field,
+ errorElement: annotationField,
),
);
}
@@ -740,9 +2095,8 @@ final class ForyGenerator extends Generator {
);
}
- _ConstructionModel _buildConstructionModel(
+ _ConstructionModel _buildExternalConstructionModel(
ClassElement declaration,
- InterfaceType targetType,
String targetTypeLiteral,
ConstructorElement constructor,
String? constructorName,
@@ -759,7 +2113,7 @@ final class ForyGenerator extends Generator {
for (final field in fields) field.name: field,
};
final arguments = <_ConstructorArgumentSpec>[];
- final constructorFieldNames = <String>{};
+ final constructorFields = <_GeneratedFieldSpec>{};
var omittedOptionalPositional = false;
for (final parameter in constructor.formalParameters) {
final parameterName = parameter.displayName;
@@ -801,10 +2155,10 @@ final class ForyGenerator extends Generator {
element: declaration,
);
}
- constructorFieldNames.add(field.name);
+ constructorFields.add(field);
arguments.add(
_ConstructorArgumentSpec(
- fieldName: field.name,
+ field: field,
parameterName: parameterName,
named: parameter.isNamed,
),
@@ -812,7 +2166,7 @@ final class ForyGenerator extends Generator {
}
for (final field in fields) {
- if (!constructorFieldNames.contains(field.name) && !field.writable) {
+ if (!constructorFields.contains(field) && !field.writable) {
throw InvalidGenerationSourceError(
'Field ${declaration.displayName}.${field.name} is not consumed by '
'${_constructorReference(targetTypeLiteral, constructorName)} and '
@@ -824,38 +2178,50 @@ final class ForyGenerator extends Generator {
}
}
- // Constructor arguments are read before the target can be published, so a
- // tracked path through carrier metadata cannot resolve back to this target.
- final selfRefField =
- fields
- .where(
- (field) => _hasTrackedTargetReference(
- field.type,
- field.fieldType,
- targetType,
- ),
- )
- .firstOrNull;
- if (selfRefField != null) {
- throw InvalidGenerationSourceError(
- 'Constructor-based generated serializers cannot bind '
- 'reference-tracked self paths before construction. '
- 'Use a writable zero-required-argument generative constructor for '
- '$targetTypeLiteral or a manual serializer. Offending field: '
- '${declaration.displayName}.${selfRefField.name}.',
- element: declaration,
- );
+ final postConstructionFields = Set<_GeneratedFieldSpec>.identity();
+ for (final field in fields) {
+ if (!constructorFields.contains(field)) {
+ postConstructionFields.add(field);
+ }
}
- final postConstructionFields = fields
- .where((field) => !constructorFieldNames.contains(field.name))
- .map((field) => field.name)
- .toList(growable: false);
-
return _ConstructionModel.constructor(
constructorName: constructorName,
arguments: arguments,
- postConstructionFieldNames: postConstructionFields,
+ postConstructionFields: postConstructionFields,
+ );
+ }
+
+ void _validateConstructorSelfReference({
+ required ClassElement declaration,
+ required InterfaceType targetType,
+ required String targetTypeLiteral,
+ required List<_GeneratedFieldSpec> fields,
+ required _ConstructionModel constructionModel,
+ }) {
+ if (constructionModel.mode != _ConstructorMode.constructor) {
+ return;
+ }
+ // Constructor arguments are read before the target can be published, so a
+ // tracked path through carrier metadata cannot resolve back to this target.
+ _GeneratedFieldSpec? selfRefField;
+ for (final field in fields) {
+ if (_hasTrackedTargetReference(field.type, field.fieldType, targetType)) {
+ selfRefField = field;
+ break;
+ }
+ }
+ if (selfRefField == null) {
+ return;
+ }
+ final fieldOwner = selfRefField.declaration.enclosingElement.displayName;
+ throw InvalidGenerationSourceError(
+ 'Constructor-based generated serializers cannot bind '
+ 'reference-tracked self paths before construction. Use a writable '
+ 'zero-parameter generative constructor for $targetTypeLiteral '
+ 'or a manual serializer. Offending field: '
+ '$fieldOwner.${selfRefField.name}.',
+ element: declaration,
);
}
@@ -1006,23 +2372,23 @@ final class ForyGenerator extends Generator {
field,
directPrimitiveRunStartByIndex[index]!,
index,
- 'value.${field.name}',
+ field.access.read('value'),
' ',
);
} else if (_usesDirectGeneratedBasicFastPath(field)) {
output.writeln(
- ' ${_directGeneratedWriteStatement(field, 'value.${field.name}')};',
+ ' ${_directGeneratedWriteStatement(field, field.access.read('value'))};',
);
} else if (_usesDirectGeneratedTypedContainerWriteFastPath(field)) {
output.writeln(
- ' ${_directGeneratedTypedContainerWriteStatement(field, index, 'value.${field.name}')};',
+ ' ${_directGeneratedTypedContainerWriteStatement(field, index, field.access.read('value'))};',
);
} else {
_writeGeneratedDescriptorValue(
output,
field,
index,
- 'value.${field.name}',
+ field.access.read('value'),
' ',
);
}
@@ -1074,36 +2440,15 @@ final class ForyGenerator extends Generator {
' ',
);
}
- if (_usesReservedGeneratedFastPath(field)) {
- _writeDirectGeneratedBufferReadStatement(
- output,
- field,
- directPrimitiveRunStartByIndex[index]!,
- index,
- 'value.${field.name}',
- ' ',
- );
- } else if (_usesDirectGeneratedBasicFastPath(field)) {
- output.writeln(
- ' value.${field.name} = ${_directGeneratedReadExpression(field)};',
- );
- } else if (_usesDirectGeneratedTypedContainerReadFastPath(field)) {
- output.writeln(
- ' value.${field.name} = ${_directGeneratedTypedContainerReadExpression(structSpec.name, field, 'fields[$index]')};',
- );
- } else if (_usesDirectGeneratedStructFieldFastPath(field)) {
- output.writeln(
- ' value.${field.name} = $readerFunctionName(readGeneratedStructDirectValue(context, fields[$index]), value.${field.name});',
- );
- } else if (_usesDirectGeneratedDeclaredReadFastPath(field)) {
- output.writeln(
- ' value.${field.name} = $readerFunctionName(readGeneratedStructDeclaredValue(context, fields[$index]), value.${field.name});',
- );
- } else {
- output.writeln(
- ' value.${field.name} = $readerFunctionName(readGeneratedStructDescriptorValue(context, fields[$index], value.${field.name}), value.${field.name});',
- );
- }
+ _writeMutableFieldRead(
+ output,
+ structSpec,
+ field,
+ index,
+ directPrimitiveRunStartByIndex[index],
+ readerFunctionName,
+ ' ',
+ );
final directPrimitiveEndRun = directPrimitiveRunByEnd[index];
if (directPrimitiveEndRun != null) {
_writeDirectGeneratedReadRunEnd(
@@ -1175,12 +2520,14 @@ final class ForyGenerator extends Generator {
final constructorInvocation = _constructorInvocation(structSpec);
output.writeln(' context.reserveGraphMemory($graphObjectBytes);');
output.writeln(' final value = $constructorInvocation;');
- for (final fieldName
- in structSpec.constructionModel.postConstructionFieldNames) {
- final field = structSpec.fields.firstWhere(
- (item) => item.name == fieldName,
- );
- output.writeln(' value.${field.name} = ${field.localName};');
+ for (final field in structSpec.fields) {
+ if (structSpec.constructionModel.postConstructionFields.contains(
+ field,
+ )) {
+ output.writeln(
+ ' ${field.access.write('value', field.localName)};',
+ );
+ }
}
output.writeln(' return value;');
}
@@ -1213,6 +2560,59 @@ final class ForyGenerator extends Generator {
}
}
+ void _writeMutableFieldRead(
+ StringBuffer output,
+ _GeneratedStructSpec structSpec,
+ _GeneratedFieldSpec field,
+ int index,
+ int? primitiveRunStart,
+ String readerFunctionName,
+ String indent,
+ ) {
+ final currentValue = field.access.read('value');
+ if (_usesReservedGeneratedFastPath(field)) {
+ final target =
+ field.access.isDirect
+ ? currentValue
+ : 'final ${field.displayType} ${field.localName}';
+ _writeDirectGeneratedBufferReadStatement(
+ output,
+ field,
+ primitiveRunStart!,
+ index,
+ target,
+ indent,
+ );
+ if (!field.access.isDirect) {
+ output.writeln(
+ '$indent${field.access.write('value', field.localName)};',
+ );
+ }
+ return;
+ }
+
+ late final String decoded;
+ if (_usesDirectGeneratedBasicFastPath(field)) {
+ decoded = _directGeneratedReadExpression(field);
+ } else if (_usesDirectGeneratedTypedContainerReadFastPath(field)) {
+ decoded = _directGeneratedTypedContainerReadExpression(
+ structSpec.name,
+ field,
+ 'fields[$index]',
+ );
+ } else if (_usesDirectGeneratedStructFieldFastPath(field)) {
+ decoded =
+ '$readerFunctionName(readGeneratedStructDirectValue(context, fields[$index]), $currentValue)';
+ } else if (_usesDirectGeneratedDeclaredReadFastPath(field)) {
+ decoded =
+ '$readerFunctionName(readGeneratedStructDeclaredValue(context, fields[$index]), $currentValue)';
+ } else {
+ decoded =
+ '$readerFunctionName(readGeneratedStructDescriptorValue(context, fields[$index], $currentValue), $currentValue)';
+ }
+ output.writeln('$indent${field.access.write('value', decoded)};');
+ }
+
void _writeCompatibleStructReadMethod(
StringBuffer output,
_GeneratedStructSpec structSpec, {
@@ -1325,25 +2725,21 @@ final class ForyGenerator extends Generator {
for (var index = 0; index < structSpec.fields.length; index += 1) {
final field = structSpec.fields[index];
output.writeln(' case ${index * 2}:');
- _writeExactCompatRead(
+ _writeMutableExactCompatRead(
output,
structSpec,
field,
index,
- 'value.${field.name}',
- 'value.${field.name}',
' ',
);
output
..writeln(' break;')
..writeln(' case ${index * 2 + 1}:');
- _writeCompatConversionRead(
+ _writeMutableCompatConversionRead(
output,
structSpec,
field,
index,
- 'value.${field.name}',
- 'value.${field.name}',
'field',
' ',
);
@@ -1359,6 +2755,75 @@ final class ForyGenerator extends Generator {
..writeln(' return value;');
}
+ void _writeMutableExactCompatRead(
+ StringBuffer output,
+ _GeneratedStructSpec structSpec,
+ _GeneratedFieldSpec field,
+ int index,
+ String indent,
+ ) {
+ final currentValue = field.access.read('value');
+ if (field.access.isDirect) {
+ _writeExactCompatRead(
+ output,
+ structSpec,
+ field,
+ index,
+ currentValue,
+ currentValue,
+ indent,
+ );
+ return;
+ }
+ final decoded = '${field.localName}Exact';
+ _writeExactCompatRead(
+ output,
+ structSpec,
+ field,
+ index,
+ 'final ${field.displayType} $decoded',
+ currentValue,
+ indent,
+ );
+ output.writeln('$indent${field.access.write('value', decoded)};');
+ }
+
+ void _writeMutableCompatConversionRead(
+ StringBuffer output,
+ _GeneratedStructSpec structSpec,
+ _GeneratedFieldSpec field,
+ int index,
+ String readField,
+ String indent,
+ ) {
+ final currentValue = field.access.read('value');
+ if (field.access.isDirect) {
+ _writeCompatConversionRead(
+ output,
+ structSpec,
+ field,
+ index,
+ currentValue,
+ currentValue,
+ readField,
+ indent,
+ );
+ return;
+ }
+ final decoded = '${field.localName}Converted';
+ _writeCompatConversionRead(
+ output,
+ structSpec,
+ field,
+ index,
+ 'final ${field.displayType} $decoded',
+ currentValue,
+ readField,
+ indent,
+ );
+ output.writeln('$indent${field.access.write('value', decoded)};');
+ }
+
void _writeCtorCompatSwitch(
StringBuffer output,
_GeneratedStructSpec structSpec,
@@ -1428,12 +2893,10 @@ final class ForyGenerator extends Generator {
' context.reserveGraphMemory(${_graphObjectBytes(structSpec)});',
);
output.writeln(' final value = $constructorInvocation;');
- for (final fieldName
- in structSpec.constructionModel.postConstructionFieldNames) {
- final field = structSpec.fields.firstWhere(
- (item) => item.name == fieldName,
- );
- output.writeln(' value.${field.name} = ${field.localName};');
+ for (final field in structSpec.fields) {
+ if (structSpec.constructionModel.postConstructionFields.contains(field)) {
+ output.writeln(' ${field.access.write('value', field.localName)};');
+ }
}
output.writeln(' return value;');
}
@@ -1630,9 +3093,7 @@ final class ForyGenerator extends Generator {
final positionalArguments = <String>[];
final namedArguments = <String>[];
for (final argument in structSpec.constructionModel.arguments) {
- final field = structSpec.fields.firstWhere(
- (item) => item.name == argument.fieldName,
- );
+ final field = argument.field;
if (argument.named) {
namedArguments.add('${argument.parameterName}: ${field.localName}');
} else {
@@ -1647,7 +3108,7 @@ final class ForyGenerator extends Generator {
}
int _graphObjectBytes(_GeneratedStructSpec structSpec) =>
- _structObjectOwnerBytes + structSpec.graphFieldCount * _referenceBytes;
+ _structObjectOwnerBytes + structSpec.storageFieldCount * _referenceBytes;
int _externalGraphFieldCount(
ClassElement declaration,
@@ -1717,12 +3178,12 @@ final class ForyGenerator extends Generator {
}
String _fieldInfoLiteral(_GeneratedFieldSpec field) {
- final idLiteral = field.id == null ? 'null' : '${field.id}';
+ final identifier = field.id?.toString() ?? field.wireName!;
return '''
GeneratedFieldInfo(
name: '${field.name}',
- identifier: '${field.identifier}',
- id: $idLiteral,
+ identifier: '$identifier',
+ id: ${field.id},
fieldType: ${_fieldTypeLiteral(field.fieldType)},
),''';
}
@@ -2060,8 +3521,14 @@ GeneratedFieldType(
bool _isGeneratedStructType(DartType type) {
final element = _withoutNullability(type).element;
- return element is ClassElement &&
- _foryStructChecker.hasAnnotationOf(element);
+ if (element is! ClassElement ||
+ !_foryStructChecker.hasAnnotationOf(element)) {
+ return false;
+ }
+ final options = _structOptions(element);
+ return options.target == null &&
+ _ownsOrdinarySerializer(element) &&
+ element.typeParameters.isEmpty;
}
bool _usesDirectGeneratedTypedContainerWriteFastPath(
@@ -3120,29 +4587,17 @@ GeneratedFieldType(
String _containerElementReaderFunctionName(
String structName,
_GeneratedFieldSpec field,
- ) {
- final fieldName =
- '${field.name[0].toUpperCase()}${field.name.substring(1)}';
- return '_read$structName${fieldName}Element';
- }
+ ) => '_read$structName${field.capitalizedCodegenName}Element';
String _containerKeyReaderFunctionName(
String structName,
_GeneratedFieldSpec field,
- ) {
- final fieldName =
- '${field.name[0].toUpperCase()}${field.name.substring(1)}';
- return '_read$structName${fieldName}Key';
- }
+ ) => '_read$structName${field.capitalizedCodegenName}Key';
String _containerValueReaderFunctionName(
String structName,
_GeneratedFieldSpec field,
- ) {
- final fieldName =
- '${field.name[0].toUpperCase()}${field.name.substring(1)}';
- return '_read$structName${fieldName}Value';
- }
+ ) => '_read$structName${field.capitalizedCodegenName}Value';
List<_GeneratedFieldSpec> _sortFields(List<_GeneratedFieldSpec> fields) {
final primitiveFields = <_GeneratedFieldSpec>[];
@@ -3151,7 +4606,7 @@ GeneratedFieldType(
for (final field in fields) {
if (_isPrimitiveTypeId(field.fieldType.typeId)) {
- if (field.nullable) {
+ if (field.fieldType.nullable) {
boxedPrimitiveFields.add(field);
} else {
primitiveFields.add(field);
@@ -3195,7 +4650,7 @@ GeneratedFieldType(
if (keyCompare != 0) {
return keyCompare;
}
- return left.name.compareTo(right.name);
+ return 0;
}
int _compareOtherFields(_GeneratedFieldSpec left, _GeneratedFieldSpec right) {
@@ -3203,7 +4658,7 @@ GeneratedFieldType(
if (keyCompare != 0) {
return keyCompare;
}
- return left.name.compareTo(right.name);
+ return 0;
}
int _compareFieldIdentity(
@@ -3224,7 +4679,7 @@ GeneratedFieldType(
if ((leftId == null || leftId < 0) && rightId != null && rightId >= 0) {
return 1;
}
- final keyCompare = left.identifier.compareTo(right.identifier);
+ final keyCompare = left.wireName!.compareTo(right.wireName!);
if (keyCompare != 0) {
return keyCompare;
}
@@ -4185,9 +5640,11 @@ GeneratedFieldType(
// Rebuilding a nullable interface type drops its alias, so preserve the
// visible alias before removing nullability from the underlying type.
final alias = type.alias;
- if (alias != null) {
+ if (alias != null &&
+ (alias.element.library == _sourceLibrary ||
+ (alias.element.isPublic && _hasImportReference(alias.element)))) {
final aliasElement = alias.element;
- final prefix = _importPrefixByElement[aliasElement];
+ final prefix = _importPrefixFor(aliasElement);
final elementName = aliasElement.displayName;
final baseName = prefix == null ? elementName : '$prefix.$elementName';
if (alias.typeArguments.isEmpty) {
@@ -4199,7 +5656,7 @@ GeneratedFieldType(
final nonNullable = _withoutNullability(type);
if (nonNullable is InterfaceType) {
final element = nonNullable.element;
- final prefix = _importPrefixByElement[element];
+ final prefix = _importPrefixFor(element);
final elementName = element.displayName;
final baseName = prefix == null ? elementName : '$prefix.$elementName';
if (nonNullable.typeArguments.isEmpty) {
@@ -4297,13 +5754,81 @@ final class _GeneratedEnumSpec {
const _GeneratedEnumSpec({required this.name, required this.usesRawValue});
}
+final class _StructOptions {
+ final bool evolving;
+ final DartType? target;
+ final String? constructorName;
+ final bool exposePrivateFields;
+
+ const _StructOptions({
+ required this.evolving,
+ required this.target,
+ required this.constructorName,
+ required this.exposePrivateFields,
+ });
+}
+
+final class _HierarchyStorage {
+ final List<_HierarchyLayer> layers;
+ final List<_DiscoveredField> fields;
+
+ const _HierarchyStorage({required this.layers, required this.fields});
+}
+
+final class _HierarchyLayer {
+ final InterfaceType type;
+ final int index;
+
+ const _HierarchyLayer({required this.type, required this.index});
+}
+
+final class _DiscoveredField {
+ final FieldElement declaration;
+ final _HierarchyLayer layer;
+ final int storageIndex;
+
+ const _DiscoveredField({
+ required this.declaration,
+ required this.layer,
+ required this.storageIndex,
+ });
+}
+
+final class _PrivateAccessCompanionSpec {
+ final String name;
+ final String receiverType;
+ final String methodTypeParameters;
+ final List<_PrivateAccessMethodSpec> methods;
+
+ const _PrivateAccessCompanionSpec({
+ required this.name,
+ required this.receiverType,
+ required this.methodTypeParameters,
+ required this.methods,
+ });
+}
+
+final class _PrivateAccessMethodSpec {
+ final String fieldName;
+ final String fieldType;
+ final String getterName;
+ final String? setterName;
+
+ const _PrivateAccessMethodSpec({
+ required this.fieldName,
+ required this.fieldType,
+ required this.getterName,
+ required this.setterName,
+ });
+}
+
final class _GeneratedStructSpec {
final String name;
final InterfaceType targetType;
final String targetTypeLiteral;
final bool evolving;
final List<_GeneratedFieldSpec> fields;
- final int graphFieldCount;
+ final int storageFieldCount;
final _ConstructionModel constructionModel;
const _GeneratedStructSpec({
@@ -4312,7 +5837,7 @@ final class _GeneratedStructSpec {
required this.targetTypeLiteral,
required this.evolving,
required this.fields,
- required this.graphFieldCount,
+ required this.storageFieldCount,
required this.constructionModel,
});
}
@@ -4327,32 +5852,71 @@ final class _GeneratedFieldSpec {
final String name;
final DartType type;
final String displayType;
- final String identifier;
+ final String? wireName;
final int? id;
- final bool nullable;
- final bool ref;
- final bool? dynamic;
final bool writable;
+ final String codegenName;
+ final FieldElement declaration;
+ final _FieldAccessPlan access;
final _GeneratedFieldTypeSpec fieldType;
const _GeneratedFieldSpec({
required this.name,
required this.type,
required this.displayType,
- required this.identifier,
+ required this.wireName,
required this.id,
- required this.nullable,
- required this.ref,
- required this.dynamic,
required this.writable,
+ required this.codegenName,
+ required this.declaration,
+ required this.access,
required this.fieldType,
});
+
String readerFunctionName(String structName) {
- final fieldName = '${name[0].toUpperCase()}${name.substring(1)}';
- return '_read$structName$fieldName';
+ return '_read$structName$capitalizedCodegenName';
}
- String get localName => '_${name}Value';
+ String get capitalizedCodegenName =>
+ '${codegenName[0].toUpperCase()}${codegenName.substring(1)}';
+
+ String get localName => '_${codegenName}Value';
+}
+
+final class _FieldAccessPlan {
+ final String fieldName;
+ final String? companion;
+ final String? getter;
+ final String? setter;
+
+ const _FieldAccessPlan.direct(this.fieldName)
+ : companion = null,
+ getter = null,
+ setter = null;
+
+ const _FieldAccessPlan.companion({
+ required this.fieldName,
+ required this.companion,
+ required this.getter,
+ required this.setter,
+ });
+
+ bool get isDirect => companion == null;
+
+ String read(String receiver) {
+ final companion = this.companion;
+ return companion == null
+ ? '$receiver.$fieldName'
+ : '$companion.${getter!}($receiver)';
+ }
+
+ String write(String receiver, String value) {
+ final companion = this.companion;
+ if (companion == null) {
+ return '$receiver.$fieldName = $value';
+ }
+ return '$companion.${setter!}($receiver, $value)';
+ }
}
final class _DirectGeneratedPrimitiveRun {
@@ -4389,27 +5953,27 @@ final class _ConstructionModel {
final _ConstructorMode mode;
final String? constructorName;
final List<_ConstructorArgumentSpec> arguments;
- final List<String> postConstructionFieldNames;
+ final Set<_GeneratedFieldSpec> postConstructionFields;
const _ConstructionModel.mutable({required this.constructorName})
: mode = _ConstructorMode.mutable,
arguments = const <_ConstructorArgumentSpec>[],
- postConstructionFieldNames = const <String>[];
+ postConstructionFields = const <_GeneratedFieldSpec>{};
const _ConstructionModel.constructor({
required this.constructorName,
required this.arguments,
- required this.postConstructionFieldNames,
+ required this.postConstructionFields,
}) : mode = _ConstructorMode.constructor;
}
final class _ConstructorArgumentSpec {
- final String fieldName;
+ final _GeneratedFieldSpec field;
final String parameterName;
final bool named;
const _ConstructorArgumentSpec({
- required this.fieldName,
+ required this.field,
required this.parameterName,
required this.named,
});
diff --git a/dart/packages/fory/lib/src/codegen/generated_support.dart b/dart/packages/fory/lib/src/codegen/generated_support.dart
index fd0c14d03..f15155d67 100644
--- a/dart/packages/fory/lib/src/codegen/generated_support.dart
+++ b/dart/packages/fory/lib/src/codegen/generated_support.dart
@@ -134,8 +134,6 @@ final class GeneratedStructFieldDescriptor {
required this.usesDeclaredType,
});
- String get name => field.name;
-
meta_types.FieldType get fieldType => field.fieldType;
}
diff --git a/dart/packages/fory/lib/src/meta/field_info.dart b/dart/packages/fory/lib/src/meta/field_info.dart
index b3b525f3e..b52ee63e3 100644
--- a/dart/packages/fory/lib/src/meta/field_info.dart
+++ b/dart/packages/fory/lib/src/meta/field_info.dart
@@ -20,9 +20,21 @@
import 'package:fory/src/meta/field_type.dart';
final class FieldInfo {
+ /// The local source name used in diagnostics.
+ ///
+ /// Remote fields use their textual wire identity because no Dart source
+ /// declaration exists.
final String name;
+
+ /// The protocol's textual id-or-name form used for encoding and hashing.
+ ///
+ /// Compatible matching uses [id] for tagged fields and this value only for
+ /// untagged fields, so the two identity domains remain distinct.
final String identifier;
+
+ /// The numeric wire tag for a tagged field.
final int? id;
+
final FieldType fieldType;
const FieldInfo({
diff --git a/dart/packages/fory/lib/src/resolver/type_resolver.dart b/dart/packages/fory/lib/src/resolver/type_resolver.dart
index 23a99c0fe..798ef7386 100644
--- a/dart/packages/fory/lib/src/resolver/type_resolver.dart
+++ b/dart/packages/fory/lib/src/resolver/type_resolver.dart
@@ -219,31 +219,48 @@ bool _fieldTypeUsesNestedTypeDefinitions(FieldType fieldType) {
return false;
}
-void _validateFieldInfo(FieldInfo field) {
- final id = field.id;
- if (id != null && id < 0) {
- throw ArgumentError('Field id for ${field.name} must be non-negative.');
- }
-}
-
-List<FieldInfo> _validateFieldInfos(Iterable<FieldInfo> fields) {
- final validated = <FieldInfo>[];
- final usedIds = <int, String>{};
+String? _fieldIdentityError(List<FieldInfo> fields) {
+ final fieldsById = <int, FieldInfo>{};
+ final fieldsByName = <String, FieldInfo>{};
for (final field in fields) {
- _validateFieldInfo(field);
final id = field.id;
if (id != null) {
- final existing = usedIds[id];
- if (existing != null && existing != field.name) {
- throw ArgumentError(
- 'Duplicate field id $id for fields $existing and ${field.name}.',
- );
+ if (id < 0) {
+ return 'Field id $id must be non-negative.';
}
- usedIds[id] = field.name;
+ if (field.identifier != id.toString()) {
+ return 'Tagged field ${field.name} has textual identifier '
+ '${field.identifier}, which must match field id $id.';
+ }
+ final existing = fieldsById[id];
+ if (existing != null) {
+ return 'Duplicate field id $id for fields ${existing.name} and '
+ '${field.name}.';
+ }
+ fieldsById[id] = field;
+ continue;
}
- validated.add(field);
+ final identifier = field.identifier;
+ if (identifier.isEmpty) {
+ return 'Field wire name must not be empty.';
+ }
+ final existing = fieldsByName[identifier];
+ if (existing != null) {
+ return 'Duplicate field wire name $identifier for fields '
+ '${existing.name} and ${field.name}.';
+ }
+ fieldsByName[identifier] = field;
+ }
+ return null;
+}
+
+List<FieldInfo> _validateLocalFieldInfos(List<FieldInfo> fields) {
+ final validated = List<FieldInfo>.unmodifiable(fields);
+ final error = _fieldIdentityError(validated);
+ if (error != null) {
+ throw ArgumentError(error);
}
- return List<FieldInfo>.unmodifiable(validated);
+ return validated;
}
final class TypeResolver {
@@ -352,7 +369,7 @@ final class TypeResolver {
resolvedTypeName == null ? null : typeNameMetaString(resolvedTypeName);
final normalizedFields =
registrationKind == RegistrationKind.struct
- ? _validateFieldInfos(fields)
+ ? _validateLocalFieldInfos(fields)
: const <FieldInfo>[];
final resolved = TypeInfo(
type: type,
@@ -1247,6 +1264,12 @@ final class TypeResolver {
throw StateError('Invalid TypeDef metadata size.');
}
header.validateBodyHash(metaBody);
+ if (isStruct) {
+ final error = _fieldIdentityError(fields);
+ if (error != null) {
+ throw StateError('Invalid remote TypeDef: $error');
+ }
+ }
final resolved =
userTypeId != null
? resolveUserById(userTypeId)
diff --git a/dart/packages/fory/lib/src/serializer/scalar_conversion.dart b/dart/packages/fory/lib/src/serializer/scalar_conversion.dart
index d4caca3b2..14b065259 100644
--- a/dart/packages/fory/lib/src/serializer/scalar_conversion.dart
+++ b/dart/packages/fory/lib/src/serializer/scalar_conversion.dart
@@ -101,23 +101,32 @@ CompatibleScalarConversion? compatibleScalarConversion(
FieldInfo remoteField,
FieldInfo localField,
) {
- if (remoteField.fieldType.ref || localField.fieldType.ref) {
+ if (!canConvertCompatibleScalarTypes(
+ remoteField.fieldType,
+ localField.fieldType,
+ )) {
return null;
}
+ return CompatibleScalarConversion(remoteField, localField);
+}
+
+bool canConvertCompatibleScalarTypes(
+ FieldType remoteType,
+ FieldType localType,
+) {
+ if (remoteType.ref || localType.ref) {
+ return false;
+ }
final exactScalarFieldType =
- remoteField.fieldType.typeId == localField.fieldType.typeId &&
- remoteField.fieldType.nullable == localField.fieldType.nullable &&
- remoteField.fieldType.ref == localField.fieldType.ref;
+ remoteType.typeId == localType.typeId &&
+ remoteType.nullable == localType.nullable;
if (exactScalarFieldType) {
- return null;
- }
- if (!_supportsCompatibleScalarConversion(
- remoteField.fieldType.typeId,
- localField.fieldType.typeId,
- )) {
- return null;
+ return false;
}
- return CompatibleScalarConversion(remoteField, localField);
+ return _supportsCompatibleScalarConversion(
+ remoteType.typeId,
+ localType.typeId,
+ );
}
CompatibleScalarReadDescriptor compatibleScalarReadDescriptor(
@@ -1243,7 +1252,7 @@ Never _throwScalarConversionError(
Object cause,
) {
throw InvalidDataException(
- 'Cannot convert compatible field ${conversion.localField.identifier} '
+ 'Cannot convert compatible field ${conversion.localField.name} '
'from type ${conversion.remoteField.fieldType.typeId} '
'to type ${conversion.localField.fieldType.typeId}: $value',
cause,
diff --git a/dart/packages/fory/lib/src/serializer/struct_serializer.dart b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
index 0a50cc646..e019927bd 100644
--- a/dart/packages/fory/lib/src/serializer/struct_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
@@ -46,10 +46,8 @@ final class StructSerializer extends Serializer<Object?> {
),
),
);
- late final Map<String, SerializationFieldInfo> _localFieldsByIdentifier =
- <String, SerializationFieldInfo>{
- for (final field in _localFields) field.identifier: field,
- };
+ Map<int, SerializationFieldInfo>? _localFieldsById;
+ Map<String, SerializationFieldInfo>? _localFieldsByName;
final Map<TypeDef, CompatibleStructReadLayout> _compatibleReadLayouts =
Map<TypeDef, CompatibleStructReadLayout>.identity();
TypeDef? _lastCompatibleRemoteTypeDef;
@@ -172,6 +170,9 @@ final class StructSerializer extends Serializer<Object?> {
}
CompatibleStructReadLayout _buildCompatibleReadLayout(TypeDef remoteTypeDef) {
+ _ensureLocalFieldMaps();
+ final localFieldsById = _localFieldsById!;
+ final localFieldsByName = _localFieldsByName!;
final fields = <CompatibleStructReadField>[];
for (
var remoteIndex = 0;
@@ -179,7 +180,11 @@ final class StructSerializer extends Serializer<Object?> {
remoteIndex += 1
) {
final remoteField = remoteTypeDef.fields[remoteIndex];
- final localField = _localFieldsByIdentifier[remoteField.identifier];
+ final remoteId = remoteField.id;
+ final localField =
+ remoteId == null
+ ? localFieldsByName[remoteField.identifier]
+ : localFieldsById[remoteId];
if (localField == null) {
fields.add(
CompatibleStructReadField(
@@ -256,6 +261,24 @@ final class StructSerializer extends Serializer<Object?> {
_lastCompatibleReadLayout = layout;
return layout;
}
+
+ void _ensureLocalFieldMaps() {
+ if (_localFieldsById != null) {
+ return;
+ }
+ final fieldsById = <int, SerializationFieldInfo>{};
+ final fieldsByName = <String, SerializationFieldInfo>{};
+ for (final field in _localFields) {
+ final id = field.id;
+ if (id == null) {
+ fieldsByName[field.identifier] = field;
+ } else {
+ fieldsById[id] = field;
+ }
+ }
+ _localFieldsById = fieldsById;
+ _localFieldsByName = fieldsByName;
+ }
}
bool _topLevelListArrayPair(FieldInfo localField, FieldInfo remoteField) {
@@ -309,11 +332,7 @@ bool _compatibleFieldType(
localType.arguments.length == remoteType.arguments.length) {
return true;
}
- return compatibleScalarConversion(
- FieldInfo(name: '', identifier: '', id: null, fieldType: remoteType),
- FieldInfo(name: '', identifier: '', id: null, fieldType: localType),
- ) !=
- null;
+ return canConvertCompatibleScalarTypes(remoteType, localType);
}
if (_isStructTypeId(localType.typeId) &&
_isStructTypeId(remoteType.typeId) &&
diff --git a/dart/packages/fory/pubspec.yaml b/dart/packages/fory/pubspec.yaml
index d497cccfd..5284df482 100644
--- a/dart/packages/fory/pubspec.yaml
+++ b/dart/packages/fory/pubspec.yaml
@@ -28,7 +28,7 @@ environment:
sdk: ^3.7.0
dependencies:
- analyzer: '>=8.0.0 <14.0.0'
+ analyzer: '>=8.1.0 <14.0.0'
build: '>=3.0.0 <5.0.0'
build_config: '>=1.2.0 <1.4.0'
meta: '>=1.14.0 <2.0.0'
@@ -36,6 +36,6 @@ dependencies:
dev_dependencies:
build_runner: '>=2.7.0 <3.0.0'
- build_test: '>=3.3.1 <3.5.16'
+ build_test: '>=3.5.0 <3.5.16'
lints: ^6.1.0
test: ^1.31.1
diff --git a/dart/packages/fory/test/codegen_test_support.dart b/dart/packages/fory/test/codegen_test_support.dart
new file mode 100644
index 000000000..f8c399b4d
--- /dev/null
+++ b/dart/packages/fory/test/codegen_test_support.dart
@@ -0,0 +1,132 @@
+// 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.
+
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:build/build.dart';
+import 'package:build_test/build_test.dart';
+import 'package:fory/src/codegen/fory_builder.dart';
+import 'package:test/test.dart';
+
+const String foryAnnotationImports = '''
+import 'package:fory/src/annotation/fory_field.dart';
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'package:fory/src/annotation/type_spec.dart';
+''';
+
+final Future<Map<String, String>> _annotationAssets = _loadAnnotationAssets();
+
+String foryGeneratedPath(String inputPath) =>
+ inputPath.replaceFirst(RegExp(r'\.dart$'), '.fory.dart');
+
+String foryTestLibrarySource(
+ String inputPath,
+ String body, {
+ String directives = '',
+ String imports = foryAnnotationImports,
+}) {
+ final fileName = inputPath.split('/').last;
+ return '''
+$imports
+$directives
+part '${foryGeneratedPath(fileName)}';
+
+$body
+''';
+}
+
+Future<String> generateForySource({
+ required String inputPath,
+ required String source,
+ Map<String, String> additionalAssets = const <String, String>{},
+}) async {
+ final result = await testBuilder(
+ foryBuilder(BuilderOptions.empty),
+ <String, String>{
+ ...(await _annotationAssets),
+ ...additionalAssets,
+ 'fory|$inputPath': source,
+ },
+ rootPackage: 'fory',
+ generateFor: <String>{'fory|$inputPath'},
+ outputs: null,
+ flattenOutput: true,
+ );
+ expect(result.succeeded, isTrue, reason: result.errors.join('\n'));
+ return result.readerWriter.testing.readString(
+ AssetId('fory', foryGeneratedPath(inputPath)),
+ );
+}
+
+Future<void> expectForyGenerationOutput({
+ required String inputPath,
+ required String source,
+ required Matcher output,
+ Map<String, String> additionalAssets = const <String, String>{},
+}) async {
+ expect(
+ await generateForySource(
+ inputPath: inputPath,
+ source: source,
+ additionalAssets: additionalAssets,
+ ),
+ output,
+ );
+}
+
+Future<void> expectForyGenerationError({
+ required String inputPath,
+ required String source,
+ required String message,
+ Map<String, String> additionalAssets = const <String, String>{},
+}) async {
+ final logs = <String>[];
+ final result = await testBuilder(
+ foryBuilder(BuilderOptions.empty),
+ <String, String>{
+ ...(await _annotationAssets),
+ ...additionalAssets,
+ 'fory|$inputPath': source,
+ },
+ rootPackage: 'fory',
+ generateFor: <String>{'fory|$inputPath'},
+ onLog: (record) => logs.add(record.message),
+ );
+ expect(result.succeeded, isFalse);
+ expect(logs.join('\n'), contains(message));
+}
+
+Future<Map<String, String>> _loadAnnotationAssets() async {
+ const paths = <String>[
+ 'src/annotation/fory_field.dart',
+ 'src/annotation/fory_struct.dart',
+ 'src/annotation/fory_union.dart',
+ 'src/annotation/type_spec.dart',
+ ];
+ final assets = <String, String>{};
+ for (final path in paths) {
+ final uri = await Isolate.resolvePackageUri(
+ Uri.parse('package:fory/$path'),
+ );
+ if (uri == null) {
+ throw StateError('Could not resolve package:fory/$path.');
+ }
+ assets['fory|lib/$path'] = await File.fromUri(uri).readAsString();
+ }
+ return assets;
+}
diff --git a/dart/packages/fory/test/external_type_generator_test.dart b/dart/packages/fory/test/external_type_generator_test.dart
index e806428b2..040041e35 100644
--- a/dart/packages/fory/test/external_type_generator_test.dart
+++ b/dart/packages/fory/test/external_type_generator_test.dart
@@ -18,14 +18,10 @@
@TestOn('vm')
library;
-import 'dart:io';
-import 'dart:isolate';
-
-import 'package:build/build.dart';
-import 'package:build_test/build_test.dart';
-import 'package:fory/src/codegen/fory_builder.dart';
import 'package:test/test.dart';
+import 'codegen_test_support.dart';
+
const String _fixtureHeader = '''
import 'package:fory/src/annotation/fory_field.dart';
import 'package:fory/src/annotation/fory_struct.dart';
@@ -35,47 +31,19 @@ import 'external_target_fixture.dart';
part 'external_serializer_fixture.fory.dart';
''';
-final Future<Map<String, String>> _annotationAssets = _loadAnnotationAssets();
-
-Future<Map<String, String>> _loadAnnotationAssets() async {
- const paths = <String>[
- 'src/annotation/fory_field.dart',
- 'src/annotation/fory_struct.dart',
- 'src/annotation/fory_union.dart',
- 'src/annotation/type_spec.dart',
- ];
- final assets = <String, String>{};
- for (final path in paths) {
- final uri = await Isolate.resolvePackageUri(
- Uri.parse('package:fory/$path'),
- );
- if (uri == null) {
- throw StateError('Could not resolve package:fory/$path.');
- }
- assets['fory|lib/$path'] = await File.fromUri(uri).readAsString();
- }
- return assets;
-}
-
Future<void> _expectGenerationError({
required String targetSource,
required String declarationSource,
required String message,
}) async {
- final logs = <String>[];
- await testBuilder(
- foryBuilder(BuilderOptions.empty),
- <String, String>{
- ...(await _annotationAssets),
+ await expectForyGenerationError(
+ inputPath: 'test/external_serializer_fixture.dart',
+ source: '$_fixtureHeader\n$declarationSource',
+ additionalAssets: <String, String>{
'fory|test/external_target_fixture.dart': targetSource,
- 'fory|test/external_serializer_fixture.dart':
- '$_fixtureHeader\n$declarationSource',
},
- rootPackage: 'fory',
- generateFor: <String>{'fory|test/external_serializer_fixture.dart'},
- onLog: (record) => logs.add(record.message),
+ message: message,
);
- expect(logs, contains(contains(message)));
}
Future<void> _expectGenerationOutput({
@@ -85,20 +53,14 @@ Future<void> _expectGenerationOutput({
String fixtureHeader = _fixtureHeader,
Map<String, String> additionalAssets = const <String, String>{},
}) async {
- await testBuilder(
- foryBuilder(BuilderOptions.empty),
- <String, String>{
- ...(await _annotationAssets),
+ await expectForyGenerationOutput(
+ inputPath: 'test/external_serializer_fixture.dart',
+ source: '$fixtureHeader\n$declarationSource',
+ additionalAssets: <String, String>{
'fory|test/external_target_fixture.dart': targetSource,
...additionalAssets,
- 'fory|test/external_serializer_fixture.dart':
- '$fixtureHeader\n$declarationSource',
- },
- rootPackage: 'fory',
- generateFor: <String>{'fory|test/external_serializer_fixture.dart'},
- outputs: <String, Matcher>{
- 'fory|test/external_serializer_fixture.fory.dart': decodedMatches(output),
},
+ output: output,
);
}
@@ -599,11 +561,11 @@ abstract final class TargetSerializer {
''',
output: allOf(
contains('context.reserveGraphMemory(44);'),
- contains("name: 'value',"),
- contains("name: 'inherited',"),
- isNot(contains("name: 'omitted',")),
+ contains("identifier: 'value',"),
+ contains("identifier: 'inherited',"),
+ isNot(contains("identifier: 'omitted',")),
isNot(contains('_hidden')),
- isNot(contains("name: 'mixed',")),
+ isNot(contains("identifier: 'mixed',")),
),
);
});
diff --git a/dart/packages/fory/test/graph_memory_budget_test.dart b/dart/packages/fory/test/graph_memory_budget_test.dart
index ee5218aac..bc2dba480 100644
--- a/dart/packages/fory/test/graph_memory_budget_test.dart
+++ b/dart/packages/fory/test/graph_memory_budget_test.dart
@@ -22,9 +22,11 @@ import 'dart:typed_data';
import 'package:fory/fory.dart';
import 'package:fory/src/context/meta_string_reader.dart';
import 'package:fory/src/context/ref_reader.dart';
+import 'package:fory/src/meta/type_def.dart';
import 'package:fory/src/resolver/type_resolver.dart';
import 'package:fory/src/serializer/collection_serializers.dart';
import 'package:fory/src/serializer/map_serializers.dart';
+import 'package:fory/src/util/hash_util.dart';
import 'package:test/test.dart';
part 'graph_memory_budget_test.fory.dart';
@@ -94,6 +96,61 @@ class BudgetCompatibleArrayEnvelope {
Int32List values = Int32List(0);
}
+class BudgetHierarchyBase {
+ BudgetHierarchyBase();
+
+ @ForyField(id: 30, type: Int32Type())
+ int baseValue = 0;
+
+ @ForyField(ignore: true)
+ int ignoredBase = 0;
+}
+
+mixin BudgetHierarchyMixin {
+ @ForyField(id: 10, type: Int32Type())
+ int mixinValue = 0;
+
+ @ForyField(ignore: true)
+ int ignoredMixin = 0;
+}
+
+@ForyStruct()
+class BudgetHierarchyChild extends BudgetHierarchyBase
+ with BudgetHierarchyMixin {
+ BudgetHierarchyChild();
+
+ @ForyField(id: 20, type: Int32Type())
+ int childValue = 0;
+}
+
+@ForyStruct()
+class BudgetHierarchyFlat {
+ BudgetHierarchyFlat();
+
+ @ForyField(id: 20, type: Int32Type())
+ int childValue = 0;
+
+ @ForyField(ignore: true)
+ int ignoredMixin = 0;
+
+ @ForyField(id: 30, type: Int32Type())
+ int baseValue = 0;
+
+ @ForyField(ignore: true)
+ int ignoredBase = 0;
+
+ @ForyField(id: 10, type: Int32Type())
+ int mixinValue = 0;
+}
+
+@ForyStruct()
+class BudgetCompatibleRemote {
+ BudgetCompatibleRemote();
+
+ @ForyField(id: 20, type: Int32Type())
+ int childValue = 0;
+}
+
final class ThrowsGraphBudget extends Matcher {
const ThrowsGraphBudget();
@@ -156,6 +213,33 @@ void _registerCompatibleArray(Fory fory) {
);
}
+void _registerHierarchy(Fory fory, Type type) {
+ GraphMemoryBudgetTestForyModule.register(fory, type, id: 410);
+}
+
+void _registerCompatibleRemote(Fory fory) {
+ GraphMemoryBudgetTestForyModule.register(
+ fory,
+ BudgetCompatibleRemote,
+ name: 'test.BudgetHierarchy',
+ );
+}
+
+void _registerCompatibleHierarchy(Fory fory) {
+ GraphMemoryBudgetTestForyModule.register(
+ fory,
+ BudgetHierarchyChild,
+ name: 'test.BudgetHierarchy',
+ );
+}
+
+TypeDef _hierarchyTypeDef(Type type) {
+ _registerHierarchy(Fory(), type);
+ final resolver = TypeResolver(Config());
+ resolver.registerGenerated(type, id: 410, namespace: null, typeName: null);
+ return resolver.typeDefForResolved(resolver.resolvedRegisteredType(type));
+}
+
ReadContext _readContext(
Buffer buffer, {
int maxGraphMemoryBytes = _defaultGraphMemoryBytes,
@@ -283,6 +367,36 @@ void main() {
expect(roundTrip.ignored, isNull);
});
+ test('reserves one flattened hierarchy owner', () {
+ final writer = Fory();
+ _registerHierarchy(writer, BudgetHierarchyChild);
+ final bytes = writer.serialize(
+ BudgetHierarchyChild()
+ ..baseValue = 30
+ ..ignoredBase = 31
+ ..mixinValue = 10
+ ..ignoredMixin = 11
+ ..childValue = 20,
+ );
+ final required = _objectGraphBytes(5);
+
+ final failingReader = Fory(maxGraphMemoryBytes: required - 1);
+ _registerHierarchy(failingReader, BudgetHierarchyChild);
+ expect(
+ () => failingReader.deserialize<BudgetHierarchyChild>(bytes),
+ _throwsGraphBudget,
+ );
+
+ final passingReader = Fory(maxGraphMemoryBytes: required);
+ _registerHierarchy(passingReader, BudgetHierarchyChild);
+ final roundTrip = passingReader.deserialize<BudgetHierarchyChild>(bytes);
+ expect(roundTrip.baseValue, equals(30));
+ expect(roundTrip.mixinValue, equals(10));
+ expect(roundTrip.childValue, equals(20));
+ expect(roundTrip.ignoredBase, isZero);
+ expect(roundTrip.ignoredMixin, isZero);
+ });
+
test('reserves map entries', () {
final value = <Object?, Object?>{'a': 1};
@@ -381,6 +495,35 @@ void main() {
);
});
+ test('compatible reads reserve the local flattened shape', () {
+ final writer = Fory(compatible: true);
+ _registerCompatibleRemote(writer);
+ final bytes = writer.serialize(BudgetCompatibleRemote()..childValue = 20);
+ final required = _objectGraphBytes(5);
+
+ final failingReader = Fory(
+ compatible: true,
+ maxGraphMemoryBytes: required - 1,
+ );
+ _registerCompatibleHierarchy(failingReader);
+ expect(
+ () => failingReader.deserialize<BudgetHierarchyChild>(bytes),
+ _throwsGraphBudget,
+ );
+
+ final passingReader = Fory(
+ compatible: true,
+ maxGraphMemoryBytes: required,
+ );
+ _registerCompatibleHierarchy(passingReader);
+ final roundTrip = passingReader.deserialize<BudgetHierarchyChild>(bytes);
+ expect(roundTrip.baseValue, isZero);
+ expect(roundTrip.mixinValue, isZero);
+ expect(roundTrip.childValue, equals(20));
+ expect(roundTrip.ignoredBase, isZero);
+ expect(roundTrip.ignoredMixin, isZero);
+ });
+
test('skips strings binary and dense typed arrays', () {
final fory = Fory(maxGraphMemoryBytes: 1);
final text = List<String>.filled(128, 'x').join();
@@ -415,4 +558,24 @@ void main() {
);
});
});
+
+ group('flattened hierarchy schema', () {
+ test('matches flat TypeDef bytes hash and global order', () {
+ final inherited = _hierarchyTypeDef(BudgetHierarchyChild);
+ final flat = _hierarchyTypeDef(BudgetHierarchyFlat);
+
+ expect(
+ inherited.fields.map((field) => field.id),
+ orderedEquals(<int>[10, 20, 30]),
+ );
+ expect(
+ flat.fields.map((field) => field.id),
+ orderedEquals(<int>[10, 20, 30]),
+ );
+ expect(inherited.encoded, orderedEquals(flat.encoded));
+ expect(inherited.header, equals(flat.header));
+ expect(schemaFingerprint(inherited), equals(schemaFingerprint(flat)));
+ expect(schemaHash(inherited), equals(schemaHash(flat)));
+ });
+ });
}
diff --git a/dart/packages/fory/test/runtime_validation_test.dart b/dart/packages/fory/test/runtime_validation_test.dart
index 13a74ce2a..f86c2f096 100644
--- a/dart/packages/fory/test/runtime_validation_test.dart
+++ b/dart/packages/fory/test/runtime_validation_test.dart
@@ -136,17 +136,6 @@ class SchemaVersionV2 {
int count = 0;
}
-@ForyStruct()
-class DuplicateFieldIdOrder {
- DuplicateFieldIdOrder();
-
- @ForyField(id: 1)
- String first = '';
-
- @ForyField(id: 1)
- String second = '';
-}
-
void _registerValidationTypes(Fory fory) {
RuntimeValidationTestForyModule.register(
fory,
@@ -217,25 +206,6 @@ Object _nestedList(int depth) {
void main() {
group('field options', () {
- test('duplicate generated field ids are rejected during registration', () {
- final fory = Fory();
-
- expect(
- () => RuntimeValidationTestForyModule.register(
- fory,
- DuplicateFieldIdOrder,
- name: 'validation.DuplicateFieldIdOrder',
- ),
- throwsA(
- isA<ArgumentError>().having(
- (error) => error.toString(),
- 'message',
- contains('Duplicate field id 1'),
- ),
- ),
- );
- });
-
test('ignored fields stay local only after round trip', () {
final fory = Fory();
_registerValidationTypes(fory);
diff --git a/dart/packages/fory/test/struct_constructor_flow_generator_test.dart b/dart/packages/fory/test/struct_constructor_flow_generator_test.dart
new file mode 100644
index 000000000..e433a4704
--- /dev/null
+++ b/dart/packages/fory/test/struct_constructor_flow_generator_test.dart
@@ -0,0 +1,544 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+
+import 'codegen_test_support.dart';
+
+const String _fixtureHeader = '''
+import 'package:fory/src/annotation/fory_field.dart';
+import 'package:fory/src/annotation/fory_struct.dart';
+
+part 'struct_constructor_flow_fixture.fory.dart';
+''';
+
+Future<void> _expectGenerationOutput({
+ required String source,
+ required Matcher output,
+}) async {
+ await expectForyGenerationOutput(
+ inputPath: 'test/struct_constructor_flow_fixture.dart',
+ source: '$_fixtureHeader\n$source',
+ output: output,
+ );
+}
+
+Future<void> _expectGenerationError({
+ required String source,
+ required String message,
+}) async {
+ await expectForyGenerationError(
+ inputPath: 'test/struct_constructor_flow_fixture.dart',
+ source: '$_fixtureHeader\n$source',
+ message: message,
+ );
+}
+
+void main() {
+ group('accepted identity flow', () {
+ test('field formal', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class FieldFormalValue {
+ final int value;
+
+ FieldFormalValue(this.value);
+}
+''',
+ output: contains('final value = FieldFormalValue(_valueValue);'),
+ );
+ });
+
+ test('multi-level super formal', () async {
+ await _expectGenerationOutput(
+ source: '''
+class SuperFormalBase<T> {
+ final T value;
+
+ SuperFormalBase(this.value);
+}
+
+class SuperFormalMiddle<T> extends SuperFormalBase<T> {
+ SuperFormalMiddle(super.value);
+}
+
+@ForyStruct()
+final class SuperFormalLeaf extends SuperFormalMiddle<int> {
+ SuperFormalLeaf(super.value);
+}
+''',
+ output: allOf(
+ contains('final int _valueValue'),
+ contains('final value = SuperFormalLeaf(_valueValue);'),
+ ),
+ );
+ });
+
+ test('renamed explicit initializer', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class RenamedInitializerValue {
+ final int stored;
+
+ RenamedInitializerValue(int decoded) : stored = decoded;
+}
+''',
+ output: contains(
+ 'final value = RenamedInitializerValue(_storedValue);',
+ ),
+ );
+ });
+
+ test('named super constructor and argument', () async {
+ await _expectGenerationOutput(
+ source: '''
+class NamedSuperBase {
+ final int stored;
+
+ NamedSuperBase.named({required this.stored});
+}
+
+@ForyStruct()
+final class NamedSuperChild extends NamedSuperBase {
+ NamedSuperChild({required int decoded})
+ : super.named(stored: decoded);
+}
+''',
+ output: contains(
+ 'final value = NamedSuperChild(decoded: _storedValue);',
+ ),
+ );
+ });
+
+ test('redirect', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class RedirectedValue {
+ final int stored;
+
+ RedirectedValue(int decoded) : this.named(decoded);
+
+ RedirectedValue.named(int forwarded) : stored = forwarded;
+}
+''',
+ output: contains('final value = RedirectedValue(_storedValue);'),
+ );
+ });
+
+ test('parenthesized direct reference', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class ParenthesizedValue {
+ final int stored;
+
+ ParenthesizedValue(int decoded) : stored = (((decoded)));
+}
+''',
+ output: contains('final value = ParenthesizedValue(_storedValue);'),
+ );
+ });
+
+ test('one final and writable destination', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class FinalAndWritableValue {
+ final int fixed;
+ int mutable;
+
+ FinalAndWritableValue(int decoded)
+ : fixed = decoded,
+ mutable = decoded;
+}
+''',
+ output: allOf(
+ contains('final value = FinalAndWritableValue(_fixedValue);'),
+ contains('value.mutable = _mutableValue;'),
+ ),
+ );
+ });
+
+ test('passes an optional writable identity flow', () async {
+ await _expectGenerationOutput(
+ source: '''
+class OptionalMutableBase {
+ int mutable;
+
+ OptionalMutableBase([int decoded = 0]) : mutable = decoded;
+}
+
+@ForyStruct()
+final class OptionalMutableChild extends OptionalMutableBase {
+ final int fixed;
+
+ OptionalMutableChild(this.fixed, [int decoded = 0]) : super(decoded);
+}
+''',
+ output: allOf(
+ contains(
+ 'final value = OptionalMutableChild('
+ '_fixedValue, _mutableValue);',
+ ),
+ isNot(contains('value.mutable = _mutableValue;')),
+ ),
+ );
+ });
+
+ test('analyzes optional flow for an all-writable struct', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class AllWritableOptionalValue {
+ int value;
+
+ @ForyField(ignore: true)
+ final int observedAtConstruction;
+
+ AllWritableOptionalValue([int decoded = 0])
+ : value = decoded,
+ observedAtConstruction = decoded;
+}
+''',
+ output: allOf(
+ contains('final value = AllWritableOptionalValue(_valueValue);'),
+ isNot(contains('value.value = _valueValue;')),
+ ),
+ );
+ });
+
+ test('keeps an unbound optional constructor mutable', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class UnboundOptionalSelf {
+ UnboundOptionalSelf({int ignored = 0});
+
+ @ForyField(ref: true)
+ UnboundOptionalSelf? next;
+}
+''',
+ output: allOf(
+ contains('final value = UnboundOptionalSelf();'),
+ contains('context.reference(value);'),
+ isNot(contains('ignored:')),
+ ),
+ );
+ });
+
+ test('omits an unbound optional named parameter', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class OptionalNamedValue {
+ final int value;
+
+ OptionalNamedValue({int ignored = 0, this.value = 0});
+}
+''',
+ output: allOf(
+ contains('final value = OptionalNamedValue(value: _valueValue);'),
+ isNot(contains('ignored:')),
+ ),
+ );
+ });
+
+ test('keeps generic constructor specializations separate', () async {
+ await _expectGenerationOutput(
+ source: '''
+class GenericBase<T> {
+ final T value;
+
+ GenericBase(T decoded) : value = decoded;
+}
+
+@ForyStruct()
+final class GenericIntChild extends GenericBase<int> {
+ GenericIntChild(int decoded) : super(decoded);
+}
+
+@ForyStruct()
+final class GenericStringChild extends GenericBase<String> {
+ GenericStringChild(String decoded) : super(decoded);
+}
+''',
+ output: allOf(
+ contains('final int _valueValue'),
+ contains('final String _valueValue'),
+ contains('final value = GenericIntChild(_valueValue);'),
+ contains('final value = GenericStringChild(_valueValue);'),
+ ),
+ );
+ });
+
+ test('writes only unbound mutable fields', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+final class CanonicalPostWrites {
+ int zeta;
+ int beta = 0;
+ int alpha = 0;
+
+ CanonicalPostWrites(this.zeta);
+}
+''',
+ output: predicate<String>((generated) {
+ final alphaWrite = generated.indexOf('value.alpha = _alphaValue;');
+ final betaWrite = generated.indexOf('value.beta = _betaValue;');
+ return alphaWrite >= 0 &&
+ betaWrite >= 0 &&
+ alphaWrite < betaWrite &&
+ !generated.contains('value.zeta = _zetaValue;');
+ }, 'writes unbound mutable fields in canonical schema order'),
+ );
+ });
+ });
+
+ group('rejected identity flow', () {
+ test('same-name unused parameter', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class SameNameUnusedValue {
+ int value = 0;
+
+ SameNameUnusedValue(int value);
+}
+''',
+ message: 'name equality is not constructor identity proof',
+ );
+ });
+
+ test('function call', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class CallValue {
+ final int value;
+
+ CallValue(int decoded) : value = identity(decoded);
+
+ static int identity(int value) => value;
+}
+''',
+ message: 'transforms or does not directly reference',
+ );
+ });
+
+ test('operator', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class OperatorValue {
+ final int value;
+
+ OperatorValue(int decoded) : value = decoded + 0;
+}
+''',
+ message: 'transforms or does not directly reference',
+ );
+ });
+
+ test('cast', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class CastValue {
+ final int value;
+
+ CastValue(Object decoded) : value = decoded as int;
+}
+''',
+ message: 'transforms or does not directly reference',
+ );
+ });
+
+ test('null assertion', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class NullAssertionValue {
+ final int value;
+
+ NullAssertionValue(int? decoded) : value = decoded!;
+}
+''',
+ message: 'transforms or does not directly reference',
+ );
+ });
+
+ test('constant', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class ConstantValue {
+ final int value;
+
+ ConstantValue(int decoded) : value = 1;
+}
+''',
+ message: 'transforms or does not directly reference',
+ );
+ });
+
+ test('constructor body assignment', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class BodyAssignmentValue {
+ late final int value;
+
+ BodyAssignmentValue(int decoded) {
+ value = decoded;
+ }
+}
+''',
+ message: 'is not initialized from a parameter',
+ );
+ });
+
+ test('final declaration initializer', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class FinalInitializerValue {
+ final int value = 1;
+
+ FinalInitializerValue();
+}
+''',
+ message: 'has a declaration initializer',
+ );
+ });
+
+ test('late-final declaration initializer', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class LateFinalInitializerValue {
+ late final int value = 1;
+
+ LateFinalInitializerValue();
+}
+''',
+ message: 'has a declaration initializer',
+ );
+ });
+
+ test('one root for multiple final fields', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class AmbiguousFinalValue {
+ final int first;
+ final int second;
+
+ AmbiguousFinalValue(int decoded)
+ : first = decoded,
+ second = decoded;
+}
+''',
+ message: 'initializes multiple non-writable fields',
+ );
+ });
+
+ test('required root with ambiguous writable fields', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class AmbiguousWritableValue {
+ int first;
+ int second;
+
+ AmbiguousWritableValue(int decoded)
+ : first = decoded,
+ second = decoded;
+}
+''',
+ message: 'has multiple writable identity-flow sources',
+ );
+ });
+
+ test('optional root with ambiguous writable fields', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class AmbiguousOptionalValue {
+ final int fixed;
+ int first;
+ int second;
+
+ AmbiguousOptionalValue(this.fixed, [int decoded = 0])
+ : first = decoded,
+ second = decoded;
+}
+''',
+ message: 'has multiple writable identity-flow sources',
+ );
+ });
+
+ test('required parameter without a field source', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class MissingRequiredSourceValue {
+ int stored = 0;
+
+ MissingRequiredSourceValue(int decoded);
+}
+''',
+ message: 'has no serialized field connected by an exact',
+ );
+ });
+
+ test('optional positional gap', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class OptionalGapValue {
+ final int value;
+
+ OptionalGapValue([int ignored = 0, this.value = 0]);
+}
+''',
+ message: 'after an omitted optional positional parameter',
+ );
+ });
+
+ test('reference-tracked self path', () async {
+ await _expectGenerationError(
+ source: '''
+@ForyStruct()
+final class SelfReferenceValue {
+ @ForyField(ref: true)
+ final SelfReferenceValue? next;
+
+ SelfReferenceValue(this.next);
+}
+''',
+ message: 'cannot bind reference-tracked self paths',
+ );
+ });
+ });
+}
diff --git a/dart/packages/fory/test/struct_inheritance_generator_test.dart b/dart/packages/fory/test/struct_inheritance_generator_test.dart
new file mode 100644
index 000000000..b5475416b
--- /dev/null
+++ b/dart/packages/fory/test/struct_inheritance_generator_test.dart
@@ -0,0 +1,625 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+
+import 'codegen_test_support.dart';
+
+const String _inputPath = 'test/struct_inheritance_generator_fixture.dart';
+
+const String _fixtureHeader = '''
+import 'package:fory/src/annotation/fory_field.dart';
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'package:fory/src/annotation/type_spec.dart';
+
+part 'struct_inheritance_generator_fixture.fory.dart';
+''';
+
+Future<void> _expectGenerationOutput({
+ required String source,
+ required Matcher output,
+ String fixtureHeader = _fixtureHeader,
+ Map<String, String> additionalAssets = const <String, String>{},
+}) async {
+ await expectForyGenerationOutput(
+ inputPath: _inputPath,
+ source: '$fixtureHeader\n$source',
+ output: output,
+ additionalAssets: additionalAssets,
+ );
+}
+
+Future<void> _expectGenerationError({
+ required String source,
+ required String message,
+ String fixtureHeader = _fixtureHeader,
+ Map<String, String> additionalAssets = const <String, String>{},
+}) async {
+ await expectForyGenerationError(
+ inputPath: _inputPath,
+ source: '$fixtureHeader\n$source',
+ message: message,
+ additionalAssets: additionalAssets,
+ );
+}
+
+Matcher _wireFieldsInOrder(List<String> names) {
+ return predicate<String>((output) {
+ var offset = 0;
+ for (final name in names) {
+ final next = output.indexOf("identifier: '$name',", offset);
+ if (next < 0) {
+ return false;
+ }
+ offset = next + 1;
+ }
+ return true;
+ }, 'contains wire fields in order: ${names.join(', ')}');
+}
+
+Matcher _containsOnce(String text) {
+ return predicate<String>(
+ (output) => RegExp(RegExp.escape(text)).allMatches(output).length == 1,
+ 'contains exactly once: $text',
+ );
+}
+
+void main() {
+ group('ordinary hierarchy discovery', () {
+ test('flattens superclass and mixin storage exactly once', () async {
+ await _expectGenerationOutput(
+ source: '''
+abstract interface class Contract {
+ Object? get interfaceOnly;
+}
+
+abstract class Grand {
+ Object? grand;
+ static Object? staticOnly;
+
+ Object? get abstractOnly;
+}
+
+class Parent extends Grand {
+ Object? parent;
+
+ @override
+ Object? get abstractOnly => parent;
+}
+
+mixin M1 {
+ Object? m1;
+}
+
+mixin M2 {
+ Object? m2;
+}
+
+@ForyStruct()
+class Child extends Parent with M1, M2 implements Contract {
+ Child();
+
+ Object? child;
+
+ @override
+ Object? get interfaceOnly => child;
+}
+''',
+ output: allOf(<Matcher>[
+ _wireFieldsInOrder(const <String>[
+ 'child',
+ 'grand',
+ 'm1',
+ 'm2',
+ 'parent',
+ ]),
+ _containsOnce("identifier: 'grand',"),
+ _containsOnce("identifier: 'parent',"),
+ _containsOnce("identifier: 'm1',"),
+ _containsOnce("identifier: 'm2',"),
+ _containsOnce("identifier: 'child',"),
+ contains(RegExp(r'value\.grand = _readChildGrand\(')),
+ contains(RegExp(r'value\.parent = _readChildParent\(')),
+ contains(RegExp(r'value\.m1 = _readChildM1\(')),
+ contains(RegExp(r'value\.m2 = _readChildM2\(')),
+ contains(RegExp(r'value\.child = _readChildChild\(')),
+ contains('context.reserveGraphMemory(44);'),
+ isNot(contains("identifier: 'interface_only',")),
+ isNot(contains("identifier: 'abstract_only',")),
+ isNot(contains("identifier: 'static_only',")),
+ ]),
+ );
+ });
+
+ test('flattens a named generic mixin application', () async {
+ await _expectGenerationOutput(
+ source: '''
+class AliasBase<T> {
+ final T baseValue;
+
+ AliasBase(this.baseValue);
+}
+
+mixin AliasMixin<T> {
+ T? mixinValue;
+}
+
+class NamedApplication<T> = AliasBase<T> with AliasMixin<T>;
+
+@ForyStruct()
+class AliasChild extends NamedApplication<int> {
+ AliasChild(super.baseValue);
+
+ int childValue = 0;
+}
+''',
+ output: allOf(
+ contains('final int _baseValueValue'),
+ contains('final int? _mixinValueValue'),
+ contains('final int _childValueValue'),
+ contains('final value = AliasChild(_baseValueValue);'),
+ contains('context.reserveGraphMemory(36);'),
+ ),
+ );
+ });
+
+ test('substitutes inherited generic fields in each layer', () async {
+ await _expectGenerationOutput(
+ source: '''
+class Base<T> {
+ late List<T> items;
+}
+
+class Middle<U> extends Base<U> {}
+
+@ForyStruct()
+class StringChild extends Middle<String> {
+ StringChild();
+}
+''',
+ output: allOf(
+ contains("identifier: 'items',"),
+ contains('type: List<String>,'),
+ contains('value.items'),
+ isNot(contains('type: List<T>,')),
+ isNot(contains('type: List<U>,')),
+ ),
+ );
+ });
+
+ test('uses public storage from another library directly', () async {
+ await _expectGenerationOutput(
+ fixtureHeader: '''
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'public_inheritance_parent.dart';
+
+part 'struct_inheritance_generator_fixture.fory.dart';
+''',
+ additionalAssets: const <String, String>{
+ 'fory|test/public_inheritance_parent.dart': '''
+class PublicParent {
+ Object? inherited;
+}
+''',
+ },
+ source: '''
+@ForyStruct()
+class PublicChild extends PublicParent {
+ PublicChild();
+}
+''',
+ output: allOf(
+ contains("identifier: 'inherited',"),
+ contains('value.inherited'),
+ isNot(contains('ForyFieldAccess')),
+ ),
+ );
+ });
+
+ test('rejects an ambiguous inherited field type name', () async {
+ await _expectGenerationError(
+ fixtureHeader: '''
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'typed_inheritance_parent.dart';
+import 'conflicting_type.dart';
+
+part 'struct_inheritance_generator_fixture.fory.dart';
+''',
+ additionalAssets: const <String, String>{
+ 'fory|test/typed_inheritance_parent.dart': '''
+class SharedValue {}
+
+class TypedParent {
+ SharedValue? inherited;
+}
+''',
+ 'fory|test/conflicting_type.dart': 'class SharedValue {}',
+ },
+ source: '''
+@ForyStruct()
+class TypedChild extends TypedParent {
+ TypedChild();
+}
+''',
+ message: 'SharedValue, which is not nameable',
+ );
+ });
+
+ test('uses an unambiguous prefix for an inherited field type', () async {
+ await _expectGenerationOutput(
+ fixtureHeader: '''
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'typed_inheritance_parent.dart';
+import 'typed_inheritance_parent.dart' as parent;
+import 'conflicting_type.dart';
+
+part 'struct_inheritance_generator_fixture.fory.dart';
+''',
+ additionalAssets: const <String, String>{
+ 'fory|test/typed_inheritance_parent.dart': '''
+class SharedValue {}
+
+class TypedParent {
+ SharedValue? inherited;
+}
+''',
+ 'fory|test/conflicting_type.dart': 'class SharedValue {}',
+ },
+ source: '''
+@ForyStruct()
+class TypedChild extends TypedParent {
+ TypedChild();
+}
+''',
+ output: allOf(
+ contains('type: parent.SharedValue,'),
+ contains('parent.SharedValue? _readTypedChildInherited('),
+ contains('value.inherited'),
+ ),
+ );
+ });
+
+ test('rejects a deferred inherited field type namespace', () async {
+ await _expectGenerationError(
+ fixtureHeader: '''
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'deferred_type_parent.dart';
+import 'shared_value.dart' deferred as values;
+
+part 'struct_inheritance_generator_fixture.fory.dart';
+''',
+ additionalAssets: const <String, String>{
+ 'fory|test/shared_value.dart': 'class SharedValue {}',
+ 'fory|test/deferred_type_parent.dart': '''
+import 'shared_value.dart';
+
+class DeferredTypeParent {
+ SharedValue? inherited;
+}
+''',
+ },
+ source: '''
+@ForyStruct()
+class DeferredTypeChild extends DeferredTypeParent {
+ DeferredTypeChild();
+}
+''',
+ message: 'SharedValue, which is not nameable',
+ );
+ });
+
+ test('uses same-library private storage directly', () async {
+ await _expectGenerationOutput(
+ source: '''
+class PrivateParent {
+ Object? _secret;
+}
+
+@ForyStruct()
+class PrivateChild extends PrivateParent {
+ PrivateChild();
+}
+''',
+ output: allOf(
+ contains("identifier: '_secret',"),
+ contains('value._secret'),
+ isNot(contains('ForyFieldAccess')),
+ ),
+ );
+ });
+
+ test('reconstructs same-library private final storage directly', () async {
+ await _expectGenerationOutput(
+ source: '''
+class PrivateFinalParent {
+ final int _secret;
+
+ PrivateFinalParent(this._secret);
+}
+
+@ForyStruct()
+class PrivateFinalChild extends PrivateFinalParent {
+ PrivateFinalChild(int decoded) : super(decoded);
+}
+''',
+ output: allOf(
+ contains("identifier: '_secret',"),
+ contains('value._secret'),
+ contains('final value = PrivateFinalChild(__secretValue);'),
+ isNot(contains('ForyFieldAccess')),
+ ),
+ );
+ });
+
+ test(
+ 'ignores only declaration-owned fields before access checks',
+ () async {
+ await _expectGenerationOutput(
+ fixtureHeader: '''
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'ignored_inheritance_parent.dart';
+
+part 'struct_inheritance_generator_fixture.fory.dart';
+''',
+ additionalAssets: const <String, String>{
+ 'fory|test/ignored_inheritance_parent.dart': '''
+import 'package:fory/src/annotation/fory_field.dart';
+
+class IgnoredParent {
+ @ForyField(ignore: true)
+ Object? _ignored;
+
+ Object? visible;
+}
+''',
+ },
+ source: '''
+@ForyStruct()
+class IgnoreChild extends IgnoredParent {
+ IgnoreChild();
+}
+''',
+ output: allOf(
+ contains("identifier: 'visible',"),
+ contains('value.visible'),
+ contains('context.reserveGraphMemory(32);'),
+ isNot(contains('_ignored')),
+ isNot(contains('ForyFieldAccess')),
+ ),
+ );
+ },
+ );
+
+ test('rejects an unsupported inherited effective type', () async {
+ await _expectGenerationError(
+ source: '''
+int _identity(int value) => value;
+
+class UnsupportedParent {
+ int Function(int) callback = _identity;
+}
+
+@ForyStruct()
+class UnsupportedChild extends UnsupportedParent {
+ UnsupportedChild();
+}
+''',
+ message: 'has unsupported effective type int Function(int)',
+ );
+ });
+
+ test(
+ 'ignored unsupported inherited storage bypasses type analysis',
+ () async {
+ await _expectGenerationOutput(
+ source: '''
+int _identity(int value) => value;
+
+class IgnoredUnsupportedParent {
+ @ForyField(ignore: true)
+ int Function(int) callback = _identity;
+}
+
+@ForyStruct()
+class IgnoredUnsupportedChild extends IgnoredUnsupportedParent {
+ IgnoredUnsupportedChild();
+
+ int value = 0;
+}
+''',
+ output: allOf(
+ contains("identifier: 'value',"),
+ contains('context.reserveGraphMemory(32);'),
+ isNot(contains('callback')),
+ ),
+ );
+ },
+ );
+ });
+
+ group('hierarchy validation', () {
+ test('rejects hidden ancestor storage', () async {
+ await _expectGenerationError(
+ source: '''
+class HiddenBase {
+ Object? value;
+}
+
+@ForyStruct()
+class HiddenChild extends HiddenBase {
+ HiddenChild();
+
+ @override
+ Object? value;
+}
+''',
+ message: 'later field or accessor hides the storage',
+ );
+ });
+
+ test('rejects a repeated mixin storage application', () async {
+ await _expectGenerationError(
+ source: '''
+class RepeatedBase {}
+
+mixin RepeatedMixin {
+ int value = 0;
+}
+
+class FirstApplication = RepeatedBase with RepeatedMixin;
+class SecondApplication = FirstApplication with RepeatedMixin;
+
+@ForyStruct()
+class RepeatedChild extends SecondApplication {}
+''',
+ message: 'the same field declaration is applied more than once',
+ );
+ });
+
+ test('rejects duplicate ids across the flattened schema', () async {
+ await _expectGenerationError(
+ source: '''
+class IdBase {
+ @ForyField(id: 7)
+ int baseValue = 0;
+}
+
+@ForyStruct()
+class IdChild extends IdBase {
+ IdChild();
+
+ @ForyField(id: 7)
+ int childValue = 0;
+}
+''',
+ message: 'duplicate field id 7',
+ );
+ });
+
+ test('rejects duplicate canonical names across hierarchy layers', () async {
+ await _expectGenerationError(
+ source: '''
+class NameBase {
+ int fooBar = 0;
+}
+
+@ForyStruct()
+class NameChild extends NameBase {
+ NameChild();
+
+ int foo_bar = 0;
+}
+''',
+ message: 'duplicate canonical field name foo_bar',
+ );
+ });
+
+ test('uses indexed generated names when source names collide', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+class CodegenNameCollision {
+ CodegenNameCollision(this.foo, this.Foo);
+
+ @ForyField(id: 1)
+ final int foo;
+
+ @ForyField(id: 2)
+ final int Foo;
+}
+''',
+ output: allOf(
+ contains('final int _field0Value'),
+ contains('final int _field1Value'),
+ contains('_readCodegenNameCollisionField0('),
+ contains('_readCodegenNameCollisionField1('),
+ contains(
+ 'final value = CodegenNameCollision(_field0Value, _field1Value);',
+ ),
+ ),
+ );
+ });
+
+ test('sorts from normalized TypeSpec nullability', () async {
+ await _expectGenerationOutput(
+ source: '''
+@ForyStruct()
+class NormalizedSort {
+ NormalizedSort();
+
+ @ForyField(type: Int64Type(nullable: true))
+ int aForcedBoxed = 0;
+
+ int zPrimitive = 0;
+}
+''',
+ output: allOf(
+ _wireFieldsInOrder(const <String>['z_primitive', 'a_forced_boxed']),
+ predicate<String>((output) {
+ final fieldStart = output.indexOf("identifier: 'a_forced_boxed',");
+ final fieldEnd = output.indexOf('),', fieldStart);
+ if (fieldStart < 0 || fieldEnd < 0) {
+ return false;
+ }
+ return output
+ .substring(fieldStart, fieldEnd)
+ .contains('nullable: true,');
+ }, 'uses normalized nullable metadata for a_forced_boxed'),
+ ),
+ );
+ });
+ });
+
+ test('keeps external target hierarchy schema explicit', () async {
+ await _expectGenerationOutput(
+ fixtureHeader: '''
+import 'package:fory/src/annotation/fory_struct.dart';
+import 'external_inheritance_target.dart';
+
+part 'struct_inheritance_generator_fixture.fory.dart';
+''',
+ additionalAssets: const <String, String>{
+ 'fory|test/external_inheritance_target.dart': '''
+class ExternalBase {
+ int inherited = 0;
+ int omittedBase = 0;
+}
+
+class ExternalChild extends ExternalBase {
+ int omittedChild = 0;
+}
+''',
+ },
+ source: '''
+@ForyStruct(target: ExternalChild)
+abstract final class ExternalChildSchema {
+ late final int inherited;
+}
+''',
+ output: allOf(
+ _containsOnce("identifier: 'inherited',"),
+ contains('value.inherited'),
+ contains('context.reserveGraphMemory(36);'),
+ isNot(contains("identifier: 'omitted_base',")),
+ isNot(contains("identifier: 'omitted_child',")),
+ ),
+ );
+ });
+}
diff --git a/dart/packages/fory/test/struct_private_companion_generator_test.dart b/dart/packages/fory/test/struct_private_companion_generator_test.dart
new file mode 100644
index 000000000..0ec50e799
--- /dev/null
+++ b/dart/packages/fory/test/struct_private_companion_generator_test.dart
@@ -0,0 +1,1005 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+
+import 'codegen_test_support.dart';
+
+const String _annotationImports = '''
+import 'package:fory/src/annotation/fory_field.dart';
+import 'package:fory/src/annotation/fory_struct.dart';
+''';
+
+String _librarySource(String inputPath, String body, {String directives = ''}) {
+ return foryTestLibrarySource(
+ inputPath,
+ body,
+ directives: directives,
+ imports: _annotationImports,
+ );
+}
+
+Future<String> _generate({
+ required String inputPath,
+ required String source,
+ Map<String, String> additionalAssets = const <String, String>{},
+}) async {
+ return generateForySource(
+ inputPath: inputPath,
+ source: source,
+ additionalAssets: additionalAssets,
+ );
+}
+
+Future<void> _expectGenerationError({
+ required String inputPath,
+ required String source,
+ required String message,
+ Map<String, String> additionalAssets = const <String, String>{},
+}) async {
+ await expectForyGenerationError(
+ inputPath: inputPath,
+ source: source,
+ message: message,
+ additionalAssets: additionalAssets,
+ );
+}
+
+String _providerSource({
+ String inputPath = 'test/private_provider.dart',
+ String boundaryName = 'Boundary',
+}) {
+ return _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class $boundaryName {
+ int _secret = 0;
+}
+''');
+}
+
+void _expectOnlyVisibilityBridge(String output, Iterable<String> owners) {
+ for (final owner in owners) {
+ expect(
+ output,
+ contains(
+ r'abstract final class $'
+ '${owner}ForyFieldAccess',
+ ),
+ );
+ expect(output, isNot(contains('_${owner}ForySerializer')));
+ }
+ expect(output, isNot(contains('GeneratedStructSchema<')));
+ expect(output, isNot(contains('GeneratedStructSerializer<')));
+ expect(output, isNot(contains('ReadContext')));
+ expect(output, isNot(contains('WriteContext')));
+ expect(output, isNot(contains('Serializer<')));
+ expect(output, isNot(contains('ForyModule')));
+ expect(output, isNot(contains('Object?')));
+ expect(output, isNot(contains('dynamic')));
+ expect(output, isNot(contains('Function')));
+ expect(output, isNot(contains('callback')));
+ expect(output, isNot(contains('reflection')));
+ expect(output, isNot(contains('register')));
+}
+
+void main() {
+ group('provider ownership', () {
+ test('supports abstract, open generic, and mixin providers', () async {
+ const inputPath = 'test/provider_shapes.dart';
+ final output = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class AbstractBoundary {
+ int _abstractValue = 0;
+}
+
+@ForyStruct(exposePrivateFields: true)
+class GenericBoundary<T extends num> {
+ T? _genericValue;
+}
+
+@ForyStruct(exposePrivateFields: true)
+mixin SecretMixin {
+ int _mixedValue = 0;
+}
+'''),
+ );
+
+ _expectOnlyVisibilityBridge(output, const <String>[
+ 'AbstractBoundary',
+ 'GenericBoundary',
+ 'SecretMixin',
+ ]);
+ expect(
+ output,
+ matches(
+ RegExp(
+ r'static T\? \$g[0-9a-f]{16}<T extends num>\('
+ r'GenericBoundary<T> value\)',
+ ),
+ ),
+ );
+ expect(output, contains('GenericBoundary<T> value'));
+ expect(output, contains('SecretMixin value'));
+ });
+
+ test('concrete provider owns both serializer and companion', () async {
+ const inputPath = 'test/concrete_provider.dart';
+ final output = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+class ConcreteProvider {
+ int _secret = 0;
+ int visible = 1;
+}
+'''),
+ );
+
+ expect(
+ output,
+ contains(r'abstract final class $ConcreteProviderForyFieldAccess'),
+ );
+ expect(output, contains('final class _ConcreteProviderForySerializer'));
+ expect(output, contains('GeneratedStructSchema<ConcreteProvider>'));
+ expect(output, contains('ConcreteProviderForyModule'));
+ });
+
+ test('concrete exposure leaves its serializer surface unchanged', () async {
+ const inputPath = 'test/concrete_surface.dart';
+ final normal = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct()
+class ConcreteSurface {
+ int _value = 0;
+}
+'''),
+ );
+ final exposed = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+class ConcreteSurface {
+ int _value = 0;
+}
+'''),
+ );
+ const marker =
+ 'const List<GeneratedFieldInfo> _concreteSurfaceForyFieldInfo';
+
+ expect(
+ exposed.substring(exposed.indexOf(marker)),
+ normal.substring(normal.indexOf(marker)),
+ );
+ expect(exposed, contains(r'class $ConcreteSurfaceForyFieldAccess'));
+ expect(normal, isNot(contains(r'$ConcreteSurfaceForyFieldAccess')));
+ });
+
+ test('exposePrivateFields defaults to false', () async {
+ const inputPath = 'test/default_false.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct()
+abstract class Boundary {
+ int _secret = 0;
+}
+'''),
+ message: 'does not own a concrete generated serializer',
+ );
+ });
+
+ test('consumer annotation cannot authorize parent storage', () async {
+ const inputPath = 'test/consumer_annotation.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+class Child extends Boundary {
+ int _childSecret = 0;
+}
+''', directives: "import 'unexposed_parent.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|test/unexposed_parent.dart': '''
+abstract class Boundary {
+ int _parentSecret = 0;
+}
+''',
+ },
+ message:
+ 'No public hierarchy boundary in the declaring library is '
+ 'annotated with @ForyStruct(exposePrivateFields: true)',
+ );
+ });
+
+ test('external target cannot expose private fields', () async {
+ const inputPath = 'test/external_exposure.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+class Target {
+ int value = 0;
+}
+
+@ForyStruct(target: Target, exposePrivateFields: true)
+abstract final class TargetSchema {
+ late final int value;
+}
+'''),
+ message:
+ 'ForyStruct.exposePrivateFields cannot be used with '
+ 'ForyStruct.target',
+ );
+ });
+
+ test('empty or fully ignored provider is rejected', () async {
+ const inputPath = 'test/empty_provider.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ int visible = 0;
+
+ @ForyField(ignore: true)
+ int _ignored = 0;
+}
+'''),
+ message: 'has no non-ignored private storage',
+ );
+ });
+
+ test('provider boundary must be public and externally usable', () async {
+ const cases = <({String name, String declaration, String message})>[
+ (
+ name: 'private',
+ declaration: '''
+@ForyStruct(exposePrivateFields: true)
+abstract class _Boundary {
+ int _secret = 0;
+}
+''',
+ message: 'requires a public hierarchy boundary',
+ ),
+ (
+ name: 'final',
+ declaration: '''
+@ForyStruct(exposePrivateFields: true)
+final class Boundary {
+ int _secret = 0;
+}
+''',
+ message: 'cannot be extended or mixed in outside',
+ ),
+ ];
+
+ for (final testCase in cases) {
+ final inputPath = 'test/${testCase.name}_provider.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, testCase.declaration),
+ message: testCase.message,
+ );
+ }
+ });
+
+ test('provider-only boundary rejects schema options', () async {
+ const cases = <({String name, String annotation, String message})>[
+ (
+ name: 'evolving',
+ annotation: '@ForyStruct(exposePrivateFields: true, evolving: false)',
+ message: 'ForyStruct.evolving has no meaning',
+ ),
+ (
+ name: 'constructor',
+ annotation:
+ "@ForyStruct(exposePrivateFields: true, constructor: 'named')",
+ message: 'ForyStruct.constructor has no meaning',
+ ),
+ ];
+
+ for (final testCase in cases) {
+ final inputPath = 'test/${testCase.name}_provider_option.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+${testCase.annotation}
+abstract class Boundary {
+ int _secret = 0;
+}
+'''),
+ message: testCase.message,
+ );
+ }
+ });
+ });
+
+ group('companion API', () {
+ test('emits mutable accessors and getter-only final access', () async {
+ const inputPath = 'test/accessor_shape.dart';
+ final output = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ int _mutable = 0;
+ final int _immutable = 1;
+}
+'''),
+ );
+
+ _expectOnlyVisibilityBridge(output, const <String>['Boundary']);
+ expect(
+ RegExp(
+ r'static int \$g[0-9a-f]{16}\(Boundary value\)',
+ ).allMatches(output),
+ hasLength(2),
+ );
+ expect(
+ RegExp(r'static void \$s[0-9a-f]{16}\(').allMatches(output),
+ hasLength(1),
+ );
+ expect(output, contains('=> value._mutable;'));
+ expect(output, contains('value._mutable = fieldValue;'));
+ expect(output, contains('=> value._immutable;'));
+ expect(output, isNot(contains('value._immutable = fieldValue;')));
+ });
+
+ test('public boundary exposes a private ancestor slot', () async {
+ const providerPath = 'test/private_ancestor_provider.dart';
+ final providerSource = _librarySource(providerPath, '''
+class _PrivateStorage {
+ int _secret = 0;
+}
+
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary extends _PrivateStorage {}
+''');
+ final providerOutput = await _generate(
+ inputPath: providerPath,
+ source: providerSource,
+ );
+
+ expect(
+ providerOutput,
+ contains(r'abstract final class $BoundaryForyFieldAccess'),
+ );
+ expect(providerOutput, contains('Boundary value'));
+ expect(providerOutput, contains('=> value._secret;'));
+ expect(providerOutput, contains('value._secret = fieldValue;'));
+ expect(
+ providerOutput,
+ isNot(contains(r'$_PrivateStorageForyFieldAccess')),
+ );
+ final getterName =
+ RegExp(
+ r'static int (\$g[0-9a-f]{16})\(Boundary value\)',
+ ).firstMatch(providerOutput)!.group(1)!;
+ final setterName =
+ RegExp(
+ r'static void (\$s[0-9a-f]{16})\(',
+ ).firstMatch(providerOutput)!.group(1)!;
+
+ const childPath = 'test/private_ancestor_child.dart';
+ final childOutput = await _generate(
+ inputPath: childPath,
+ source: _librarySource(childPath, '''
+@ForyStruct()
+class Child extends Boundary {
+ int local = 0;
+}
+''', directives: "import 'private_ancestor_provider.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|$providerPath': providerSource,
+ },
+ );
+
+ expect(childOutput, contains(r'$BoundaryForyFieldAccess.' + getterName));
+ expect(childOutput, contains(r'$BoundaryForyFieldAccess.' + setterName));
+ expect(childOutput, isNot(contains('value._secret')));
+ expect(childOutput, isNot(contains('_BoundaryForySerializer')));
+ });
+
+ test('uses one companion for each declaring library', () async {
+ const aPath = 'test/library_a.dart';
+ const bPath = 'test/library_b.dart';
+ const childPath = 'test/multi_library_child.dart';
+ final aSource = _librarySource(aPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class ABoundary {
+ int _a = 0;
+}
+''');
+ final bSource = _librarySource(
+ bPath,
+ '''
+@ForyStruct(exposePrivateFields: true)
+abstract class BBoundary extends ABoundary {
+ int _b = 0;
+}
+''',
+ directives: '''
+import 'library_a.dart';
+export 'library_a.dart';
+''',
+ );
+ final output = await _generate(
+ inputPath: childPath,
+ source: _librarySource(childPath, '''
+@ForyStruct()
+class Child extends BBoundary {}
+''', directives: "import 'library_b.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|$aPath': aSource,
+ 'fory|$bPath': bSource,
+ },
+ );
+
+ expect(output, contains(r'$ABoundaryForyFieldAccess.$g'));
+ expect(output, contains(r'$ABoundaryForyFieldAccess.$s'));
+ expect(output, contains(r'$BBoundaryForyFieldAccess.$g'));
+ expect(output, contains(r'$BBoundaryForyFieldAccess.$s'));
+ expect(output, isNot(contains('_ABoundaryForySerializer')));
+ expect(output, isNot(contains('_BBoundaryForySerializer')));
+ });
+
+ test('uses stable distinct getter and setter digests', () async {
+ const inputPath = 'test/digest_provider.dart';
+ final source = _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ int _first = 0;
+ int _second = 0;
+}
+''');
+ final first = await _generate(inputPath: inputPath, source: source);
+ final second = await _generate(inputPath: inputPath, source: source);
+
+ expect(second, first);
+ final getterDigests =
+ RegExp(
+ r'static int \$g([0-9a-f]{16})\(',
+ ).allMatches(first).map((match) => match.group(1)!).toSet();
+ final setterDigests =
+ RegExp(
+ r'static void \$s([0-9a-f]{16})\(',
+ ).allMatches(first).map((match) => match.group(1)!).toSet();
+ expect(getterDigests, hasLength(2));
+ expect(setterDigests, getterDigests);
+ });
+ });
+
+ group('consumer resolution', () {
+ final providerSource = _providerSource();
+
+ test('resolves direct, prefixed, re-exported, and shown helpers', () async {
+ final cases = <
+ ({
+ String name,
+ String directives,
+ Map<String, String> assets,
+ String parent,
+ String expected,
+ })
+ >[
+ (
+ name: 'direct',
+ directives: "import 'private_provider.dart';",
+ assets: const <String, String>{},
+ parent: 'Boundary',
+ expected: r'$BoundaryForyFieldAccess.$g',
+ ),
+ (
+ name: 'prefixed',
+ directives: "import 'private_provider.dart' as p;",
+ assets: const <String, String>{},
+ parent: 'p.Boundary',
+ expected: r'p.$BoundaryForyFieldAccess.$g',
+ ),
+ (
+ name: 'reexported',
+ directives: "import 'private_barrel.dart';",
+ assets: const <String, String>{
+ 'fory|test/private_barrel.dart': "export 'private_provider.dart';",
+ },
+ parent: 'Boundary',
+ expected: r'$BoundaryForyFieldAccess.$g',
+ ),
+ (
+ name: 'shown',
+ directives:
+ r"import 'private_provider.dart' "
+ r'show Boundary, $BoundaryForyFieldAccess;',
+ assets: const <String, String>{},
+ parent: 'Boundary',
+ expected: r'$BoundaryForyFieldAccess.$g',
+ ),
+ (
+ name: 'split_unprefixed',
+ directives:
+ "import 'private_provider.dart' show Boundary;\n"
+ r"import 'private_provider.dart' "
+ r'show $BoundaryForyFieldAccess;',
+ assets: const <String, String>{},
+ parent: 'Boundary',
+ expected: r'$BoundaryForyFieldAccess.$g',
+ ),
+ (
+ name: 'split_prefixed',
+ directives:
+ "import 'private_provider.dart' as p show Boundary;\n"
+ r"import 'private_provider.dart' as p "
+ r'show $BoundaryForyFieldAccess;',
+ assets: const <String, String>{},
+ parent: 'p.Boundary',
+ expected: r'p.$BoundaryForyFieldAccess.$g',
+ ),
+ ];
+
+ for (final testCase in cases) {
+ final inputPath = 'test/${testCase.name}_consumer.dart';
+ final output = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct()
+class Child extends ${testCase.parent} {}
+''', directives: testCase.directives),
+ additionalAssets: <String, String>{
+ 'fory|test/private_provider.dart': providerSource,
+ ...testCase.assets,
+ },
+ );
+
+ expect(output, contains(testCase.expected));
+ expect(
+ output,
+ contains(testCase.expected.replaceFirst(r'.$g', r'.$s')),
+ );
+ expect(output, isNot(contains('_BoundaryForySerializer')));
+ }
+ });
+
+ test('rejects show, hide, and re-export paths missing helper', () async {
+ final cases =
+ <({String name, String directives, Map<String, String> assets})>[
+ (
+ name: 'show_missing',
+ directives: "import 'private_provider.dart' show Boundary;",
+ assets: const <String, String>{},
+ ),
+ (
+ name: 'hide_helper',
+ directives:
+ r"import 'private_provider.dart' "
+ r'hide $BoundaryForyFieldAccess;',
+ assets: const <String, String>{},
+ ),
+ (
+ name: 'reexport_missing',
+ directives: "import 'private_hidden_barrel.dart';",
+ assets: const <String, String>{
+ 'fory|test/private_hidden_barrel.dart':
+ "export 'private_provider.dart' show Boundary;",
+ },
+ ),
+ ];
+
+ for (final testCase in cases) {
+ final inputPath = 'test/${testCase.name}_consumer.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct()
+class Child extends Boundary {}
+''', directives: testCase.directives),
+ additionalAssets: <String, String>{
+ 'fory|test/private_provider.dart': providerSource,
+ ...testCase.assets,
+ },
+ message:
+ 'generated companion is not visible through the child library '
+ 'imports and re-exports',
+ );
+ }
+ });
+
+ test('rejects an ambiguous companion import namespace', () async {
+ const inputPath = 'test/ambiguous_consumer.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct()
+class Child extends Boundary {}
+''', directives: "import 'ambiguous_barrel.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|test/private_provider.dart': providerSource,
+ 'fory|test/conflicting_helper.dart': r'''
+abstract final class $BoundaryForyFieldAccess {}
+''',
+ 'fory|test/ambiguous_barrel.dart': '''
+export 'private_provider.dart';
+export 'conflicting_helper.dart';
+''',
+ },
+ message: 'is ambiguous in import namespace',
+ );
+ });
+
+ test('uses a valid prefix when another prefix is ambiguous', () async {
+ const inputPath = 'test/valid_and_ambiguous_consumer.dart';
+ final output = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(
+ inputPath,
+ '''
+@ForyStruct()
+class Child extends p.Boundary {}
+''',
+ directives: '''
+import 'private_provider.dart' as p;
+import 'ambiguous_barrel.dart' as q;
+''',
+ ),
+ additionalAssets: <String, String>{
+ 'fory|test/private_provider.dart': providerSource,
+ 'fory|test/conflicting_helper.dart': r'''
+abstract final class $BoundaryForyFieldAccess {}
+''',
+ 'fory|test/ambiguous_barrel.dart': '''
+export 'private_provider.dart';
+export 'conflicting_helper.dart';
+''',
+ },
+ );
+
+ expect(output, contains(r'p.$BoundaryForyFieldAccess.$g'));
+ expect(output, isNot(contains(r'q.$BoundaryForyFieldAccess.$g')));
+ });
+
+ test('rejects a local value shadowing the companion', () async {
+ const inputPath = 'test/local_shadow_consumer.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, r'''
+Object get $BoundaryForyFieldAccess => Object();
+
+@ForyStruct()
+class Child extends Boundary {}
+''', directives: "import 'private_provider.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|test/private_provider.dart': providerSource,
+ },
+ message: 'collides with',
+ );
+ });
+
+ test(
+ 'rejects an import prefix shadowing the unprefixed companion',
+ () async {
+ const inputPath = 'test/prefix_shadow_consumer.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(
+ inputPath,
+ '''
+@ForyStruct()
+class Child extends Boundary {}
+''',
+ directives: r'''
+import 'private_provider.dart';
+import 'unrelated.dart' as $BoundaryForyFieldAccess;
+''',
+ ),
+ additionalAssets: <String, String>{
+ 'fory|test/private_provider.dart': providerSource,
+ 'fory|test/unrelated.dart': 'class Unrelated {}',
+ },
+ message: 'collides with',
+ );
+ },
+ );
+
+ test(
+ 'uses a prefix when a local value shadows the unprefixed name',
+ () async {
+ const inputPath = 'test/local_shadow_prefixed_consumer.dart';
+ final output = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, r'''
+Object get $BoundaryForyFieldAccess => Object();
+
+@ForyStruct()
+class Child extends p.Boundary {}
+''', directives: "import 'private_provider.dart' as p;"),
+ additionalAssets: <String, String>{
+ 'fory|test/private_provider.dart': providerSource,
+ },
+ );
+
+ expect(output, contains(r'p.$BoundaryForyFieldAccess.$g'));
+ },
+ );
+
+ test('rejects a locally shadowed boundary namespace', () async {
+ const providerPath = 'test/shadowed_boundary_provider.dart';
+ const middlePath = 'test/shadowed_boundary_middle.dart';
+ const childPath = 'test/shadowed_boundary_child.dart';
+ final provider = _providerSource(inputPath: providerPath);
+ await _expectGenerationError(
+ inputPath: childPath,
+ source: _librarySource(childPath, '''
+class Boundary {}
+
+@ForyStruct()
+class Child extends PublicSubclass {}
+''', directives: "import 'shadowed_boundary_middle.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|$providerPath': provider,
+ 'fory|$middlePath': '''
+import 'shadowed_boundary_provider.dart';
+export 'shadowed_boundary_provider.dart';
+
+class PublicSubclass extends Boundary {}
+''',
+ },
+ message:
+ 'generated companion is not visible through the child library '
+ 'imports and re-exports',
+ );
+ });
+
+ test('uses a public alias without importing its underlying type', () async {
+ const providerPath = 'test/public_alias_provider.dart';
+ const childPath = 'test/public_alias_consumer.dart';
+ final provider = _librarySource(providerPath, '''
+typedef PublicAlias = PublicValue;
+
+@ForyStruct(exposePrivateFields: true)
+abstract class AliasBoundary {
+ PublicAlias? _value;
+}
+''', directives: "import 'public_alias_value.dart';");
+ final output = await _generate(
+ inputPath: childPath,
+ source: _librarySource(childPath, '''
+@ForyStruct()
+class AliasChild extends AliasBoundary {}
+''', directives: "import 'public_alias_provider.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|$providerPath': provider,
+ 'fory|test/public_alias_value.dart': 'class PublicValue {}',
+ },
+ );
+
+ expect(output, contains('final PublicAlias? __valueValue'));
+ expect(output, contains(r'$AliasBoundaryForyFieldAccess.$g'));
+ });
+ });
+
+ group('public signature validation', () {
+ test('rejects a private nominal field type', () async {
+ const inputPath = 'test/private_nominal.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+class _Hidden {}
+
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ _Hidden? _value;
+}
+'''),
+ message: 'would expose private nominal type _Hidden',
+ );
+ });
+
+ test('rejects a nested private nominal field type', () async {
+ const inputPath = 'test/nested_private_nominal.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+class _Hidden {}
+
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ List<_Hidden> _values = <_Hidden>[];
+}
+'''),
+ message: 'would expose private nominal type _Hidden',
+ );
+ });
+
+ test('rejects a private generic bound', () async {
+ const inputPath = 'test/private_bound.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+class _Hidden {}
+
+@ForyStruct(exposePrivateFields: true)
+class Boundary<T extends _Hidden> {
+ T? _value;
+}
+'''),
+ message: 'would expose private nominal type _Hidden',
+ );
+ });
+
+ test('expands a private nonnominal alias to its public type', () async {
+ const inputPath = 'test/private_alias.dart';
+ final output = await _generate(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+typedef _Count = int;
+
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ _Count _count = 0;
+}
+'''),
+ );
+
+ expect(
+ output,
+ matches(RegExp(r'static int \$g[0-9a-f]{16}\(Boundary value\)')),
+ );
+ expect(output, isNot(contains('static _Count ')));
+ });
+
+ test('rejects record signatures unsupported by ordinary structs', () async {
+ const inputPath = 'test/record_signature.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ (int,) _value = (0,);
+}
+'''),
+ message: 'ordinary ForyStruct fields do not support that type',
+ );
+ });
+
+ test('rejects nominal Function signatures', () async {
+ const inputPath = 'test/function_signature.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ Function _callback = () {};
+}
+'''),
+ message: 'Companion signatures must be exact, public, and non-callback',
+ );
+ });
+
+ test('rejects Null signatures unsupported by ordinary structs', () async {
+ const inputPath = 'test/null_signature.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ Null _value;
+}
+'''),
+ message: 'ordinary ForyStruct fields do not support that type',
+ );
+ });
+
+ test('rejects callback signatures instead of adding a carrier', () async {
+ const inputPath = 'test/private_callback.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, '''
+typedef _Callback = int Function(int);
+
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ _Callback _callback = _identity;
+}
+
+int _identity(int value) => value;
+'''),
+ message: 'Companion signatures must be exact, public, and non-callback',
+ );
+ });
+ });
+
+ group('exact storage and naming', () {
+ test('rejects hiding after the authorized boundary', () async {
+ const providerPath = 'test/hiding_provider.dart';
+ const childPath = 'test/hiding_child.dart';
+ final providerSource = _librarySource(providerPath, '''
+class _Storage {
+ int _secret = 0;
+}
+
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary extends _Storage {}
+
+class LaterBoundary extends Boundary {
+ @override
+ int get _secret => 1;
+
+ @override
+ set _secret(int value) {}
+}
+''');
+ await _expectGenerationError(
+ inputPath: childPath,
+ source: _librarySource(childPath, '''
+@ForyStruct()
+class Child extends LaterBoundary {}
+''', directives: "import 'hiding_provider.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|$providerPath': providerSource,
+ },
+ message: 'does not expose that exact slot through its effective getter',
+ );
+ });
+
+ test('rejects an ignored hider after the authorized boundary', () async {
+ const providerPath = 'test/ignored_hiding_provider.dart';
+ const childPath = 'test/ignored_hiding_child.dart';
+ final providerSource = _librarySource(providerPath, '''
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ int _secret = 0;
+}
+
+class LaterBoundary extends Boundary {
+ @ForyField(ignore: true)
+ int _secret = 1;
+}
+''');
+ await _expectGenerationError(
+ inputPath: childPath,
+ source: _librarySource(childPath, '''
+@ForyStruct()
+class Child extends LaterBoundary {}
+''', directives: "import 'ignored_hiding_provider.dart';"),
+ additionalAssets: <String, String>{
+ 'fory|$providerPath': providerSource,
+ },
+ message: 'does not expose that exact slot through its effective getter',
+ );
+ });
+
+ test('rejects a user declaration colliding with helper name', () async {
+ const inputPath = 'test/helper_collision.dart';
+ await _expectGenerationError(
+ inputPath: inputPath,
+ source: _librarySource(inputPath, r'''
+abstract final class $BoundaryForyFieldAccess {}
+
+@ForyStruct(exposePrivateFields: true)
+abstract class Boundary {
+ int _secret = 0;
+}
+'''),
+ message: 'collides with an existing top-level declaration',
+ );
+ });
+ });
+}
diff --git a/dart/packages/fory/test/xlang_protocol_test.dart b/dart/packages/fory/test/xlang_protocol_test.dart
index 3251c3a30..e33992445 100644
--- a/dart/packages/fory/test/xlang_protocol_test.dart
+++ b/dart/packages/fory/test/xlang_protocol_test.dart
@@ -53,6 +53,24 @@ final class _SchemaRemoteB {}
final class _SchemaRemoteC {}
+final class _DuplicateIdSchema {}
+
+final class _DuplicateNameSchema {}
+
+final class _InconsistentTagSchema {}
+
+final class _IdentityDomainLocal {}
+
+final class _IdentityDomainRemote {}
+
+final class _RemoteDuplicateIdLocal {}
+
+final class _RemoteDuplicateIdWriter {}
+
+final class _RemoteDuplicateNameLocal {}
+
+final class _RemoteDuplicateNameWriter {}
+
final class _LateDartExt {}
final class _LateDartHolder {}
@@ -109,6 +127,13 @@ GeneratedFieldInfo _generatedField(String name) => GeneratedFieldInfo(
fieldType: _intFieldType,
);
+GeneratedFieldInfo _taggedField(int id) => GeneratedFieldInfo(
+ name: 'tagged$id',
+ identifier: '$id',
+ id: id,
+ fieldType: _intFieldType,
+);
+
GeneratedFieldInfo _generatedMapField(String name) => GeneratedFieldInfo(
name: name,
identifier: name,
@@ -258,6 +283,92 @@ void _readTypeMeta(TypeResolver resolver, Uint8List bytes) {
);
}
+Uint8List _rewriteTypeDefBody(
+ Uint8List typeMetaBytes,
+ void Function(Uint8List body) rewrite,
+) {
+ final source = Buffer.wrap(typeMetaBytes);
+ final typeId = source.readVarUint32Small7();
+ final marker = source.readVarUint32Small14();
+ if (marker != 0) {
+ throw StateError('Expected an inline TypeDef.');
+ }
+ final header = TypeHeader(source.readInt64());
+ final bodyLength = header.readMetaSize(source);
+ final body = Uint8List.fromList(source.readBytes(bodyLength));
+ if (source.readableBytes != 0) {
+ throw StateError('Expected one complete TypeDef.');
+ }
+ rewrite(body);
+
+ final result = Buffer();
+ result.writeVarUint32Small7(typeId);
+ result.writeVarUint32(marker);
+ result.writeInt64(typeDefHeader(body));
+ if (body.length >= 0xff) {
+ result.writeVarUint32(body.length - 0xff);
+ }
+ result.writeBytes(body);
+ return result.toBytes();
+}
+
+void _replaceUniqueBytes(
+ Uint8List bytes,
+ List<int> source,
+ List<int> replacement,
+) {
+ if (source.length != replacement.length) {
+ throw ArgumentError('Replacement byte lengths must match.');
+ }
+ var match = -1;
+ for (var offset = 0; offset <= bytes.length - source.length; offset += 1) {
+ var equal = true;
+ for (var index = 0; index < source.length; index += 1) {
+ if (bytes[offset + index] != source[index]) {
+ equal = false;
+ break;
+ }
+ }
+ if (!equal) {
+ continue;
+ }
+ if (match >= 0) {
+ throw StateError('Expected a unique byte sequence.');
+ }
+ match = offset;
+ }
+ if (match < 0) {
+ throw StateError('Byte sequence was not found.');
+ }
+ bytes.setRange(match, match + replacement.length, replacement);
+}
+
+Uint8List _duplicateTaggedTypeDef(Uint8List validBytes) {
+ const tagOneHeader = (3 << 6) | (1 << 2);
+ const tagTwoHeader = (3 << 6) | (2 << 2);
+ return _rewriteTypeDefBody(
+ validBytes,
+ (body) => _replaceUniqueBytes(
+ body,
+ const <int>[tagTwoHeader, TypeIds.int32],
+ const <int>[tagOneHeader, TypeIds.int32],
+ ),
+ );
+}
+
+Uint8List _duplicateNamedTypeDef(Uint8List validBytes) {
+ final first = encodeFieldNameMetaString('alpha');
+ final second = encodeFieldNameMetaString('bravo');
+ if (first.encoding != second.encoding ||
+ first.bytes.length != second.bytes.length) {
+ throw StateError('Test field names must use the same encoding shape.');
+ }
+ return _rewriteTypeDefBody(
+ validBytes,
+ (body) => _replaceUniqueBytes(body, second.bytes, first.bytes),
+ );
+}
+
void main() {
group('xlang protocol regressions', () {
test('deserializes NONE wire values as null', () {
@@ -389,6 +500,173 @@ void main() {
);
});
+ test('rejects duplicate local field ids', () {
+ final resolver = TypeResolver(Config());
+ _rememberSchema(_DuplicateIdSchema, <GeneratedFieldInfo>[
+ _taggedField(1),
+ _taggedField(1),
+ ]);
+
+ expect(
+ () => resolver.registerGenerated(
+ _DuplicateIdSchema,
+ namespace: 'example',
+ typeName: 'DuplicateId',
+ ),
+ throwsA(
+ isA<ArgumentError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Duplicate field id 1'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects duplicate local field names', () {
+ final resolver = TypeResolver(Config());
+ _rememberSchema(_DuplicateNameSchema, <GeneratedFieldInfo>[
+ _generatedField('value'),
+ _generatedMapField('value'),
+ ]);
+
+ expect(
+ () => resolver.registerGenerated(
+ _DuplicateNameSchema,
+ namespace: 'example',
+ typeName: 'DuplicateName',
+ ),
+ throwsA(
+ isA<ArgumentError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Duplicate field wire name value'),
+ ),
+ ),
+ );
+ });
+
+ test('rejects inconsistent local tagged identity', () {
+ final resolver = TypeResolver(Config());
+ _rememberSchema(_InconsistentTagSchema, <GeneratedFieldInfo>[
+ const GeneratedFieldInfo(
+ name: 'tagged',
+ identifier: '2',
+ id: 1,
+ fieldType: _intFieldType,
+ ),
+ ]);
+
+ expect(
+ () => resolver.registerGenerated(
+ _InconsistentTagSchema,
+ namespace: 'example',
+ typeName: 'InconsistentTag',
+ ),
+ throwsA(
+ isA<ArgumentError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('textual identifier 2, which must match field id 1'),
+ ),
+ ),
+ );
+ });
+
+ test('keeps tagged ids separate from field names', () {
+ const name = 'example.IdentityDomains';
+ final reader = TypeResolver(Config());
+ _rememberSchema(_IdentityDomainLocal, <GeneratedFieldInfo>[
+ _taggedField(1),
+ _generatedMapField('1'),
+ ]);
+ reader.registerGenerated(
+ _IdentityDomainLocal,
+ namespace: 'example',
+ typeName: 'IdentityDomains',
+ );
+ final remote = _typeMetaBytes(
+ _IdentityDomainRemote,
+ name,
+ <GeneratedFieldInfo>[_generatedMapField('1'), _taggedField(1)],
+ );
+
+ _readTypeMeta(reader, remote);
+ });
+
+ test('rejects duplicate remote field ids before caching', () {
+ const name = 'example.RemoteDuplicateId';
+ final reader = TypeResolver(Config(maxSchemaVersionsPerType: 1));
+ _rememberSchema(_RemoteDuplicateIdLocal, <GeneratedFieldInfo>[
+ _generatedField('value'),
+ ]);
+ reader.registerGenerated(
+ _RemoteDuplicateIdLocal,
+ namespace: 'example',
+ typeName: 'RemoteDuplicateId',
+ );
+ final valid = _typeMetaBytes(
+ _RemoteDuplicateIdWriter,
+ name,
+ <GeneratedFieldInfo>[_taggedField(1), _taggedField(2)],
+ );
+ final duplicate = _duplicateTaggedTypeDef(valid);
+
+ expect(
+ () => _readTypeMeta(reader, duplicate),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Duplicate field id 1'),
+ ),
+ ),
+ );
+ expect(
+ () => _readTypeMeta(reader, duplicate),
+ throwsA(isA<StateError>()),
+ );
+ _readTypeMeta(reader, valid);
+ });
+
+ test('rejects duplicate remote field names before caching', () {
+ const name = 'example.RemoteDuplicateName';
+ final reader = TypeResolver(Config(maxSchemaVersionsPerType: 1));
+ _rememberSchema(_RemoteDuplicateNameLocal, <GeneratedFieldInfo>[
+ _generatedField('value'),
+ ]);
+ reader.registerGenerated(
+ _RemoteDuplicateNameLocal,
+ namespace: 'example',
+ typeName: 'RemoteDuplicateName',
+ );
+ final valid = _typeMetaBytes(
+ _RemoteDuplicateNameWriter,
+ name,
+ <GeneratedFieldInfo>[
+ _generatedField('alpha'),
+ _generatedField('bravo'),
+ ],
+ );
+ final duplicate = _duplicateNamedTypeDef(valid);
+
+ expect(
+ () => _readTypeMeta(reader, duplicate),
+ throwsA(
+ isA<StateError>().having(
+ (error) => error.toString(),
+ 'message',
+ contains('Duplicate field wire name alpha'),
+ ),
+ ),
+ );
+ expect(
+ () => _readTypeMeta(reader, duplicate),
+ throwsA(isA<StateError>()),
+ );
+ _readTypeMeta(reader, valid);
+ });
+
test('remote schema limit rejects extra versions', () {
const name = 'example.Unknown';
final reader = TypeResolver(Config(maxSchemaVersionsPerType: 1));
diff --git a/dart/packages/fory-test/pubspec.yaml b/dart/packages/inheritance-test-models/analysis_options.yaml
similarity index 71%
copy from dart/packages/fory-test/pubspec.yaml
copy to dart/packages/inheritance-test-models/analysis_options.yaml
index 204c72226..8ecc37147 100644
--- a/dart/packages/fory-test/pubspec.yaml
+++ b/dart/packages/inheritance-test-models/analysis_options.yaml
@@ -15,21 +15,9 @@
# specific language governing permissions and limitations
# under the License.
-name: fory_test
-description: Apache Fory Dart consumer and integration tests
-version: 1.5.0-dev
+include: package:lints/recommended.yaml
-resolution: workspace
-
-environment:
- sdk: ^3.7.0
-
-dependencies:
- external_type_test_models: 1.0.0
- fory: 1.5.0-dev
-
-dev_dependencies:
- build_runner: '>=2.7.0 <3.0.0'
- lints: ^6.1.0
- test: ^1.31.1
- vm_service: ^15.0.0
+analyzer:
+ exclude:
+ - .dart_tool/**
+ - lib/**/*.fory.dart
diff --git a/dart/packages/inheritance-test-models/lib/base_models.dart b/dart/packages/inheritance-test-models/lib/base_models.dart
new file mode 100644
index 000000000..2cc70ec57
--- /dev/null
+++ b/dart/packages/inheritance-test-models/lib/base_models.dart
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+library;
+
+import 'package:fory/fory.dart';
+
+part 'base_models.fory.dart';
+
+class _PrivateGenericStorage<T> {
+ _PrivateGenericStorage(T storedValue) : _ownerValue = storedValue;
+
+ @ForyField(id: 1)
+ final T _ownerValue;
+
+ @ForyField(id: 2)
+ String _sharedName = 'base-default';
+
+ @ForyField(id: 4)
+ String publicLabel = '';
+
+ T get ownerValue => _ownerValue;
+
+ String get basePrivateName => _sharedName;
+
+ void setBasePrivateName(String value) {
+ _sharedName = value;
+ }
+}
+
+@ForyStruct(exposePrivateFields: true)
+abstract class PublicGenericBoundary<T> extends _PrivateGenericStorage<T> {
+ // Keep explicit forwarding to exercise constructor identity analysis.
+ // ignore: use_super_parameters
+ PublicGenericBoundary(T value) : super(value);
+}
+
+@ForyStruct(exposePrivateFields: true)
+mixin PublicPrivateMixin {
+ @ForyField(id: 3, type: Int32Type())
+ int _mixinCount = 0;
+
+ int get mixinCount => _mixinCount;
+
+ void setMixinCount(int value) {
+ _mixinCount = value;
+ }
+}
+
+@ForyStruct(exposePrivateFields: true)
+abstract class XlangInheritanceBoundary {
+ @ForyField(id: 2, type: Int32Type())
+ int _parentInt = 0;
+
+ @ForyField(id: 6)
+ bool _parentFlag = false;
+
+ @ForyField(id: 10)
+ String _parentText = '';
+
+ int get parentInt => _parentInt;
+
+ bool get parentFlag => _parentFlag;
+
+ String get parentText => _parentText;
+
+ void setParentValues({
+ required int parentInt,
+ required bool parentFlag,
+ required String parentText,
+ }) {
+ _parentInt = parentInt;
+ _parentFlag = parentFlag;
+ _parentText = parentText;
+ }
+}
diff --git a/dart/packages/inheritance-test-models/lib/middle_models.dart b/dart/packages/inheritance-test-models/lib/middle_models.dart
new file mode 100644
index 000000000..66f3c9019
--- /dev/null
+++ b/dart/packages/inheritance-test-models/lib/middle_models.dart
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+library;
+
+import 'package:fory/fory.dart';
+import 'package:inheritance_test_models/base_models.dart';
+
+part 'middle_models.fory.dart';
+
+@ForyStruct(exposePrivateFields: true)
+abstract class PublicMiddleBoundary extends PublicGenericBoundary<String>
+ with PublicPrivateMixin {
+ // Keep explicit forwarding to exercise constructor identity analysis.
+ // ignore: use_super_parameters
+ PublicMiddleBoundary(String baseValue, int middleValue)
+ : _middleValue = middleValue,
+ super(baseValue);
+
+ @ForyField(id: 11, type: Int32Type())
+ final int _middleValue;
+
+ @ForyField(id: 12)
+ String _sharedName = 'middle-default';
+
+ @ForyField(id: 14, type: Int32Type())
+ int publicMiddleCount = 0;
+
+ int get middleValue => _middleValue;
+
+ String get middlePrivateName => _sharedName;
+
+ void setMiddlePrivateName(String value) {
+ _sharedName = value;
+ }
+}
diff --git a/dart/packages/fory-test/pubspec.yaml b/dart/packages/inheritance-test-models/pubspec.yaml
similarity index 80%
copy from dart/packages/fory-test/pubspec.yaml
copy to dart/packages/inheritance-test-models/pubspec.yaml
index 204c72226..bcf8be7e0 100644
--- a/dart/packages/fory-test/pubspec.yaml
+++ b/dart/packages/inheritance-test-models/pubspec.yaml
@@ -15,9 +15,10 @@
# specific language governing permissions and limitations
# under the License.
-name: fory_test
-description: Apache Fory Dart consumer and integration tests
-version: 1.5.0-dev
+name: inheritance_test_models
+description: Provider-owned Dart hierarchy models used by Fory inheritance tests.
+version: 1.0.0
+publish_to: none
resolution: workspace
@@ -25,11 +26,9 @@ environment:
sdk: ^3.7.0
dependencies:
- external_type_test_models: 1.0.0
- fory: 1.5.0-dev
+ fory:
+ path: ../fory
dev_dependencies:
- build_runner: '>=2.7.0 <3.0.0'
+ build_runner: ">=2.7.0 <3.0.0"
lints: ^6.1.0
- test: ^1.31.1
- vm_service: ^15.0.0
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index 42d9e857f..1e3a14799 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -26,7 +26,7 @@ environment:
# Add regular dependencies here.
dependencies:
fory: 1.5.0-dev
- build_runner: '>=2.7.0 <3.0.0'
+ build_runner: ">=2.7.0 <3.0.0"
dev_dependencies:
lints: ^6.1.0
test: ^1.31.1
@@ -34,3 +34,4 @@ workspace:
- packages/external-type-test-models
- packages/fory
- packages/fory-test
+ - packages/inheritance-test-models
diff --git a/docs/guide/dart/code-generation.md b/docs/guide/dart/code-generation.md
index 01624e300..63e5aee95 100644
--- a/docs/guide/dart/code-generation.md
+++ b/docs/guide/dart/code-generation.md
@@ -56,15 +56,101 @@ For a class owned by another library, define an
[external structural serializer](external-types.md) with
`@ForyStruct(target: ExternalType)`.
+## Inherited Fields
+
+For an ordinary struct, Fory follows the concrete superclass and applied-mixin
+storage chain. All non-static instance storage is flattened into the concrete
+child's one generated schema, globally ordered with fields declared directly
+on the child. Interfaces and abstract accessors do not add storage.
+
+Field annotations belong to the field declaration. In particular,
+`@ForyField(ignore: true)` on the declaring field is the only way to omit
+ordinary storage. Fory reports a generation error instead of silently omitting
+a field that is inaccessible, hidden, unsupported, or cannot be reconstructed.
+
+| Inherited field | Requirement |
+| --------------------------------------- | ------------------------------------------------------------------------- |
+| Public field | No parent annotation; generated child code accesses it directly |
+| Private field in the child's library | No parent annotation; the generated part accesses it directly |
+| Private field in another Dart library | A public boundary in that declaring library uses `exposePrivateFields` |
+| Field marked `@ForyField(ignore: true)` | Omitted from the schema; no access or construction requirement is checked |
+
+To expose private state from a library that owns a base class:
+
+```dart
+// package:model_owner/base.dart
+import 'package:fory/fory.dart';
+
+part 'base.fory.dart';
+
+@ForyStruct(exposePrivateFields: true)
+abstract class AccountBase {
+ AccountBase(String tenantId) : _tenantId = tenantId;
+
+ final String _tenantId;
+
+ String get tenantId => _tenantId;
+}
+```
+
+The consumer annotates only its concrete type:
+
+```dart
+// lib/account.dart
+import 'package:fory/fory.dart';
+import 'package:model_owner/base.dart';
+
+part 'account.fory.dart';
+
+@ForyStruct()
+final class Account extends AccountBase {
+ Account(String tenantId) : super(tenantId);
+}
+```
+
+`exposePrivateFields` defaults to `false` and authorizes only the generated
+private-field access companion owned by that library. It does not control field
+discovery or same-library private access. Placing it on `Account` cannot expose
+private fields owned by `model_owner`. If a hierarchy contains private fields
+from several libraries, each declaring library needs its own public opted-in
+boundary.
+
+The child source must import the provider namespace without hiding the
+generated companion. A barrel is valid when it re-exports both the public
+boundary and the companion. Across packages, generate and publish the
+provider's `.fory.dart` part before building a consumer.
+
+For example, a restrictive barrel for the model above must include both names:
+
+```dart
+export 'base.dart'
+ show AccountBase, $AccountBaseForyFieldAccess;
+```
+
+Non-ignored `final` and `late final` fields require an identity-preserving path
+from a parameter of the concrete child's selected generative constructor to
+the exact field. Initializing formals, super formals, redirects, and direct
+constructor initializers are supported. A parameter with the same name is not
+proof if the constructor ignores or transforms it. Use
+`@ForyField(ignore: true)` or a manual serializer when the object cannot be
+reconstructed without custom logic.
+
+The concrete child is registered independently and owns the only serializer
+for its flattened schema. Parent serializers are not nested or invoked.
+Inherited reference annotations feed the existing reference behavior exactly
+like fields declared directly on the child.
+
## Step 2 — Run the Generator
From the directory that contains your `pubspec.yaml`:
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
-This emits a `.fory.dart` file next to your source file. Re-run this command any time you add or rename annotated types.
+This emits a `.fory.dart` file next to your source file. Re-run this command any
+time you add or rename annotated types, change hierarchy storage, or change an
+exposure boundary.
## Step 3 — Register and Use
@@ -107,6 +193,9 @@ class Event {
When using evolving structs, also assign stable field IDs with `@ForyField(id: ...)` before you ship your first payload — those IDs are how Fory matches fields after a schema change.
+Inherited and direct fields share this one ID namespace. Do not reuse an ID in
+another superclass, mixin, or child field.
+
## Choosing Generated or Manual Serialization
Use an [external structural serializer](external-types.md) when another
diff --git a/docs/guide/dart/external-types.md b/docs/guide/dart/external-types.md
index 38314a33b..827c058e0 100644
--- a/docs/guide/dart/external-types.md
+++ b/docs/guide/dart/external-types.md
@@ -62,14 +62,21 @@ must declare each schema field as `late final` without an initializer. Each
serialized field name and Dart type, including nullability and generic
arguments, must exactly match an accessible getter on the target.
-Public target fields count toward the graph-memory budget automatically. Use
+The declaration's field list is the complete external schema. Fory does not
+automatically add fields from the target, its superclass, or its mixins. You
+may explicitly declare an accessible inherited target property just like any
+other schema field. `exposePrivateFields` applies only to ordinary Dart
+hierarchies and is invalid with `ForyStruct.target`.
+
+Public target fields count toward the graph-memory budget automatically, but
+that accounting does not add them to the serialized schema. Use
`@ForyField(ignore: true)` on an extra declaration field to count other storage
without serializing it.
Run the generator as usual:
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
## Register and Use the Target
diff --git a/docs/guide/dart/grpc-support.md b/docs/guide/dart/grpc-support.md
index 43f2a3a17..58f77a2a1 100644
--- a/docs/guide/dart/grpc-support.md
+++ b/docs/guide/dart/grpc-support.md
@@ -78,7 +78,7 @@ Then run `build_runner` once to emit the Fory serializer part file for the
generated models (this step is required before the code can run):
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
For this schema, the Dart generator emits (the model file and module are named
@@ -281,7 +281,7 @@ grpc-dart APIs, but `fory` intentionally does not depend on gRPC.
### Generated Code References a Missing `.fory.dart` Part
-Run `dart run build_runner build --delete-conflicting-outputs` after generating
+Run `dart run build_runner build` after generating
or regenerating the Dart sources. The serializer part file is produced by
`build_runner`, not by `foryc`.
diff --git a/docs/guide/dart/index.md b/docs/guide/dart/index.md
index 8c201f6ab..8097bc48e 100644
--- a/docs/guide/dart/index.md
+++ b/docs/guide/dart/index.md
@@ -29,6 +29,7 @@ languages.
- **Xlang**: serialize in Dart, deserialize in Java, Go, C#, and more without writing any glue code
- **Platform support**: use the same generated-serializer API on Dart VM/AOT, Flutter, and web
- **Fast**: generated serializer code replaces reflection during serialization
+- **Inheritance**: ordinary structs flatten concrete superclass and mixin storage into one schema
- **Schema evolution**: add or remove fields without breaking existing messages
- **Circular references**: optional reference tracking handles shared or recursive object graphs
- **Escape hatch**: write a manual serializer for any type that cannot be annotated
@@ -106,17 +107,24 @@ void main() {
Generate the companion file before running the program:
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
`PersonForyModule` is generated by `build_runner`. The `name` value is how peers in other languages identify the same type; keep it stable once your service is in production. Use `.` inside `name` to add a namespace prefix.
+An ordinary annotated class includes its concrete superclass and applied-mixin
+storage. Public and same-library private inherited fields need no parent
+annotation. See [Code Generation](code-generation.md) for cross-library private
+fields and constructor requirements.
+
## API Overview
- `Fory(...)` — create a serializer instance; create once and reuse it
- `fory.serialize(value)` — returns `Uint8List` bytes
- `fory.deserialize<T>(bytes)` — returns a `T`
- `@ForyStruct()` — marks a class for code generation
+- `@ForyStruct(exposePrivateFields: true)` — lets another library's generated
+ child serializer access private state owned by this library
- `@ForyStruct(target: Type)` — generates an external structural serializer
- `@ForyField(...)` — per-field options and canonical `type:` overrides
- `@ListField(...)`, `@SetField(...)`, `@MapField(...)` — container sugar for nested `type:` trees
diff --git a/docs/guide/dart/schema-evolution.md b/docs/guide/dart/schema-evolution.md
index 6239dc45c..741bf9cef 100644
--- a/docs/guide/dart/schema-evolution.md
+++ b/docs/guide/dart/schema-evolution.md
@@ -62,6 +62,11 @@ class UserProfile {
If you add field IDs after payloads are already in production, existing stored messages won't have them and evolution won't work correctly.
+For an ordinary inherited struct, the concrete child's schema contains every
+non-ignored field from its superclass and applied-mixin storage chain. Assign
+IDs across that complete flattened hierarchy; an ID used by a parent or mixin
+cannot be reused by the child.
+
For an [external structural serializer](external-types.md), the local
serializer declaration supplies the evolving schema and each declaration field
must match the corresponding target property.
@@ -83,6 +88,9 @@ must match the corresponding target property.
values exactly.
- Change the registration identity (`id` or `name`) of a type after messages are in production.
- Change a field's logical meaning without changing its ID.
+- Introduce field hiding that leaves both an ancestor and child storage slot;
+ generation rejects this shape because both slots cannot be addressed
+ faithfully.
## Xlang Notes
@@ -102,6 +110,21 @@ Use `compatible: false` only when every reader and writer always uses the same s
final fory = Fory(compatible: false);
```
+## Regenerating Inherited Schemas
+
+Regenerate every affected `.fory.dart` part after adding inherited storage,
+changing a superclass or mixin, or changing `exposePrivateFields`. Generate a
+dependency package's provider part before building its consumers.
+
+Older Dart generated code did not include ordinary inherited fields. After
+regeneration, compatible schemas use their normal missing-field and
+unknown-field behavior. Fixed schemas written without the inherited fields are
+not compatible with the corrected flattened schema; Fory does not add a
+special legacy reader for those payloads.
+
+This migration changes generated field discovery only. It does not change the
+runtime reference protocol.
+
## Related Topics
- [Configuration](configuration.md)
diff --git a/docs/guide/dart/schema-metadata.md b/docs/guide/dart/schema-metadata.md
index 9d189ffa8..61f93c522 100644
--- a/docs/guide/dart/schema-metadata.md
+++ b/docs/guide/dart/schema-metadata.md
@@ -21,6 +21,10 @@ license: |
Add `@ForyField(...)` to a field inside a `@ForyStruct()` class to change how that field is serialized.
+For ordinary inheritance, metadata stays with the storage declaration and is
+applied when any concrete annotated child discovers that field. A child
+redeclaration does not replace or merge the ancestor field's metadata.
+
The same annotations apply to fields in an
[external structural serializer declaration](external-types.md).
@@ -38,10 +42,12 @@ The same annotations apply to fields in an
## `ignore`
-Exclude a field from serialization entirely. Useful for cached, computed, or
-UI-only values that should not land in a persisted or transmitted message. The
-field still contributes to graph-memory accounting; because it has no wire
-representation, do not combine `ignore` with other `ForyField` options.
+Exclude the declaring field from serialization entirely. This is the only
+supported way to omit ordinary instance storage, including inherited storage.
+Useful examples include cached, computed, or UI-only values that should not
+land in a persisted or transmitted message. The field still contributes to
+graph-memory accounting; because it has no wire representation, do not combine
+`ignore` with other `ForyField` options.
```dart
@ForyField(ignore: true)
@@ -59,6 +65,9 @@ String name = '';
Once a payload is shared across services, never reuse an `id` for a different field.
+An ordinary child has one flattened field namespace. IDs must therefore be
+unique across its child, superclass, and applied-mixin declarations.
+
## `nullable`
Explicitly marks a field as nullable or non-nullable, overriding what Fory infers from the Dart type. Use this when the Dart type is non-nullable but you want Fory to accept `null` on the wire (e.g., reading messages from an older producer that can omit the field).
@@ -81,6 +90,10 @@ List<Object?> sharedNodes = <Object?>[];
Note: scalar types like `int`, `double`, and `bool` never benefit from reference tracking even if `ref: true` is set.
+An inherited `ref` annotation behaves exactly like the same annotation on a
+field declared directly by the concrete child. Inheritance does not introduce
+another reference-tracking mode or owner.
+
## `dynamic`
Controls whether Fory writes the concrete type of the field value into the payload.
diff --git a/docs/guide/dart/supported-types.md b/docs/guide/dart/supported-types.md
index 55f3858b8..6af5f9377 100644
--- a/docs/guide/dart/supported-types.md
+++ b/docs/guide/dart/supported-types.md
@@ -130,11 +130,27 @@ class User {
}
```
+Ordinary annotated classes may extend classes and apply mixins. Fory includes
+every concrete instance storage field from that hierarchy in one child schema,
+including inherited generic fields after specialization. Public fields and
+private fields in the child's Dart library are accessed directly.
+Cross-library private storage is supported only when its declaring library
+provides a public boundary annotated with
+`@ForyStruct(exposePrivateFields: true)`.
+
+Abstract getters, interfaces, static members, and other declarations without
+instance storage are not fields. A real non-ignored storage field must have a
+supported type, an unambiguous access path, and a valid reconstruction path;
+otherwise generation fails. In particular, each `final` or `late final` field
+must receive the decoded value unchanged through the concrete child's
+constructor chain.
+
See [Code Generation](code-generation.md).
Classes owned by another library can use an
[external structural serializer](external-types.md) when their public fields
-and construction match a local Fory schema declaration.
+and construction match a local Fory schema declaration. External declarations
+list their schema fields explicitly and do not scan the target hierarchy.
## Collections
diff --git a/docs/guide/dart/troubleshooting.md b/docs/guide/dart/troubleshooting.md
index fba1ef59b..5e184fe8d 100644
--- a/docs/guide/dart/troubleshooting.md
+++ b/docs/guide/dart/troubleshooting.md
@@ -33,7 +33,7 @@ The writer is sending a native-mode payload. Make sure every peer writes the xla
Fory does not know how to serialize or deserialize this type. Fix it by:
-1. Running code generation if you haven't: `dart run build_runner build --delete-conflicting-outputs`
+1. Running code generation if you haven't: `dart run build_runner build`
2. Calling the generated `register` function (or `registerSerializer`) for the type **before** calling `serialize` or `deserialize`.
3. Registering **all** types that appear in a message, not just the root type. For example, if `Order` contains an `Address`, register both.
@@ -42,11 +42,55 @@ Fory does not know how to serialize or deserialize this type. Fix it by:
Regenerate code:
```bash
-cd dart/packages/fory
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
-If you moved files or renamed types, rebuild before re-running analysis or tests.
+Run the command from the package that owns the source. If a dependency exposes
+private hierarchy fields, generate that provider package first and ensure its
+published source contains the generated `.fory.dart` part, then regenerate the
+consumer. If you moved files, renamed types, or changed a hierarchy, rebuild
+before re-running analysis or tests.
+
+## Inherited private field is not accessible
+
+Ordinary hierarchy discovery includes private storage even when it is declared
+in another Dart library. Privacy affects generated access, not whether the
+field exists in the schema.
+
+- A private field declared in the same library as the child needs no parent
+ annotation.
+- For a private field declared in another library, annotate a public hierarchy
+ boundary in that declaring library with
+ `@ForyStruct(exposePrivateFields: true)`.
+- Import that provider directly, or use a barrel that re-exports both the
+ public boundary and its generated `$<Boundary>ForyFieldAccess` companion.
+- If several libraries declare private fields in the hierarchy, each library
+ must provide its own boundary.
+
+Putting `exposePrivateFields: true` only on the consumer child cannot authorize
+another library's private state. To omit a field intentionally, place
+`@ForyField(ignore: true)` on its declaration. Fory does not silently omit an
+inaccessible inherited field.
+
+## Final inherited field cannot be reconstructed
+
+A non-ignored `final` or `late final` field must receive its decoded value
+unchanged from a parameter of the concrete child's selected generative
+constructor. Fory can follow initializing formals, super formals, redirects,
+and direct constructor initializers across the hierarchy.
+
+A parameter with the same name and type is insufficient if it is unused or its
+value is cast, transformed, or passed through a function. Forward the decoded
+value directly to the exact field, mark the field declaration with
+`@ForyField(ignore: true)`, or use a manual serializer.
+
+## Inherited field is hidden
+
+A subclass field or accessor can hide an ancestor storage slot while both
+physical slots remain on the object. Fory rejects this shape instead of
+choosing one slot and losing the other. Rename or remove the hiding member,
+ignore the ancestor field at its declaration when omission is intentional, or
+use a manual serializer.
## External target generation fails
@@ -141,7 +185,7 @@ matrix and platform guidance.
Main Dart package:
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
dart analyze
dart test
```
@@ -150,7 +194,7 @@ Integration test package:
```bash
cd dart/packages/fory-test
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
dart test
```
diff --git a/docs/guide/dart/type-registration.md b/docs/guide/dart/type-registration.md
index 7948e7afa..0a2f6bcae 100644
--- a/docs/guide/dart/type-registration.md
+++ b/docs/guide/dart/type-registration.md
@@ -65,6 +65,16 @@ Call the generated `register` function from the `.fory.dart` file. It installs a
UserModelsForyModule.register(fory, User, id: 100);
```
+For an ordinary inherited type, register the concrete annotated child. Its
+generated serializer already owns the complete flattened child schema; Fory
+does not require runtime registration of a superclass or mixin merely because
+it contributes fields.
+
+Register an independently annotated concrete parent only when values whose
+runtime type is that parent are also serialized. A provider-only
+`@ForyStruct(exposePrivateFields: true)` boundary supplies generated field
+access and has no registration entry of its own.
+
External structural serializers use the same generated registration API. Pass
the external target type:
@@ -97,6 +107,8 @@ See [Manual Serializers](custom-serializers.md) for how to implement a serialize
- Register **before** the first `serialize` or `deserialize` call.
- Register **every** class that can appear in a message, not only the root type.
+- Do not register generated private-field access companions; register only
+ concrete serialized types.
- Keep IDs (or names) **stable** once payloads are persisted or exchanged across services. Changing them will break deserialization of old messages.
- Do not mix a numeric ID on one side with a name on the other for the same type.
diff --git a/docs/guide/dart/web-platform-support.md b/docs/guide/dart/web-platform-support.md
index f329be4d5..0d0190d27 100644
--- a/docs/guide/dart/web-platform-support.md
+++ b/docs/guide/dart/web-platform-support.md
@@ -36,6 +36,8 @@ Fory Dart supports:
- Flutter web applications.
- Generated `@ForyStruct` serializers and manually registered serializers on
all supported targets.
+- Ordinary generated inheritance, including typed cross-library private-field
+ companions.
- External structural serializers generated with `@ForyStruct(target: ...)`.
## Code Generation Is Required
@@ -75,13 +77,20 @@ Generate the companion file before building or testing:
```bash
cd dart/packages/fory
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
```
The registration call is the same on VM/AOT, Flutter, and web. Manual
serializers use `registerSerializer(...)`; generated structs use the generated
`register` wrapper.
+Inherited fields use the same statically generated code on every platform.
+Public and same-library private fields are accessed directly. A provider
+library annotated with `@ForyStruct(exposePrivateFields: true)` emits typed
+static access methods for its own private state; it does not use mirrors,
+callbacks, runtime member lookup, or a parent serializer. Generate a dependency
+provider before compiling its consumer.
+
## 64-Bit Integer Rules
Dart VM `int` values are signed 64-bit values. Dart web `int` values are backed
@@ -182,11 +191,23 @@ web:
```bash
cd dart/packages/fory
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
dart test
dart test -p chrome
```
+For an application smoke test, also compile and execute the actual generated
+entry point:
+
+```bash
+dart compile js --fatal-warnings bin/app.dart -o build/app.js
+node build/app.js
+```
+
+Use a model with the same hierarchy and imports as the production application
+when validating cross-library private fields. Compilation alone does not prove
+that registration and round-trip execution work.
+
If Chrome tests fail with a stale generated file or missing part file, rerun
`build_runner` and then retry the test command from `dart/packages/fory`.
diff --git a/docs/guide/dart/xlang-serialization.md b/docs/guide/dart/xlang-serialization.md
index 639878b5d..2bea4ca01 100644
--- a/docs/guide/dart/xlang-serialization.md
+++ b/docs/guide/dart/xlang-serialization.md
@@ -83,7 +83,9 @@ ExternalSerializersForyModule.register(
```
The declaration's field IDs, names, nullability, and wire-width annotations
-define the Dart-side xlang schema.
+define the Dart-side xlang schema. An external declaration may explicitly list
+an accessible inherited target property, but Fory does not automatically scan
+the external target hierarchy.
## Dart to Java Example
@@ -189,6 +191,17 @@ Fory matches fields by name or by stable field ID. For robust cross-language int
5. Use `Timestamp`, `LocalDate`, and `Duration` for temporal fields rather than raw `DateTime`.
6. Validate real round trips across all languages before shipping.
+For an ordinary Dart class, Fory flattens concrete superclass and applied-mixin
+storage into the annotated child's one struct schema. Parent and child fields
+share one field-ID namespace and one canonical ordering, so the peer language
+should define the equivalent flat field set. A parent is not encoded as a
+nested object.
+
+Inherited `@ForyField(ref: true)` and nested container reference metadata use
+the same reference behavior as fields declared directly on the child.
+Inheritance does not change xlang reference framing or add parent-level
+reference state.
+
## Type Mapping Notes for Dart
Because Dart `int` is not itself a promise about the exact xlang wire width, prefer explicit field metadata when exact cross-language interpretation matters:
@@ -233,7 +246,7 @@ Before relying on a cross-language contract in production, test a payload end-to
Run the Dart side:
```bash
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
dart analyze
dart test
```
diff --git a/docs/specification/xlang_implementation_guide.md b/docs/specification/xlang_implementation_guide.md
index 311f69a65..8778a3741 100644
--- a/docs/specification/xlang_implementation_guide.md
+++ b/docs/specification/xlang_implementation_guide.md
@@ -1368,7 +1368,7 @@ The normal Dart integration path is:
2. annotate field overrides with `@ForyField`
3. run `build_runner`
4. call the generated per-library helper, such as
- `<InputFile>Fory.register(...)`, to bind private generated metadata and
+ `<InputFile>ForyModule.register(...)`, to bind private generated metadata and
register generated types
Generated code should emit:
@@ -1382,6 +1382,182 @@ The public helper should be a thin generated wrapper around the Fory
registration API, not a public global registry or a second unrelated
registration API family.
+### Dart Ordinary Struct Inheritance
+
+Ordinary Dart `ForyStruct` inheritance is a code-generation-time field
+discovery, normalization, access, construction, and flattening change. It does
+not redesign the runtime reference protocol.
+
+For a concrete annotated child, the generator walks the instantiated
+superclass and applied-mixin storage chain rather than only the child's direct
+`element.fields`. Each layer's `InterfaceType.element.fields` exposes its
+declared elements, including private declarations in another library. Dart
+privacy controls which generated expression can access an element; it does not
+control whether the element is discovered.
+
+The storage collector:
+
+1. visits the instantiated superclass;
+2. visits actually applied mixins in application order;
+3. visits the current class;
+4. collects concrete instance storage declared by each layer exactly once.
+
+It excludes `Object`, interfaces, mixin `on` constraints, abstract accessors,
+static fields, external fields, and synthetic members that do not own storage.
+A mixin slot is identified by its application site and declaring field, so
+multiple applications cannot be collapsed by spelling or `baseElement`.
+
+Every discovered storage field follows one pipeline:
+
+```text
+complete hierarchy discovery
+ -> declaration-owned @ForyField(ignore: true)
+ -> concrete generic substitution
+ -> direct or companion access resolution
+ -> constructor validation
+ -> one globally sorted child schema
+```
+
+`@ForyField(ignore: true)` on the declaring field is the only stage that may
+remove real ordinary storage. Ignored fields bypass type, access, construction,
+wire identity, and codec work but remain in the concrete object's shallow
+graph-memory field count. An inaccessible, private, final, hidden, unsupported,
+or otherwise unresolved non-ignored field is a generation error.
+
+Ownership is fixed:
+
+| Concern | Owner |
+| -------------------------------- | ------------------------------------------------ |
+| Field identity and annotations | Declaring storage field |
+| Hierarchy discovery/substitution | Concrete-child generator |
+| Cross-library private permission | Public boundary in the field's declaring library |
+| Schema, sort, and codec | Concrete annotated child |
+| Construction | Selected concrete-child generative constructor |
+| Reference analysis/publication | Existing concrete-child serializer path |
+| Graph-memory self charge | One concrete child object |
+| External target field list | Explicit external serializer declaration |
+
+Public inherited fields and private inherited fields declared in the child's
+library use direct generated access and require no parent annotation. A private
+field declared in another library remains discovered, but requires
+`ForyStruct(exposePrivateFields: true)` on a public hierarchy boundary in that
+field's declaring library. The option defaults to `false`, is invalid with
+`ForyStruct.target`, and authorizes only provider-library access generation. It
+does not enable discovery, alter same-library access, change field inclusion,
+or let a consumer authorize another library's private state.
+
+A public boundary may expose same-library private storage inherited from a
+private class or mixin. If private fields come from several Dart libraries,
+each declaring library must independently provide an opted-in public boundary
+and visible companion. The nearest qualifying child-visible boundary in the
+declaring library is selected.
+
+The provider's `.fory.dart` part emits a public `@nodoc` typed static access
+companion. It emits an exact getter for each exposed non-ignored private field,
+a setter only for mutable storage, and no setter for `final` or `late final`
+storage. The receiver is the public boundary type. Generic bounds and every
+type nested in a public signature must be nameable outside the provider
+library. The companion must not use `dynamic`, `Object?` bridge casts,
+reflection, callbacks, runtime lookup, a parent serializer, or stored runtime
+state.
+
+The child source must have a direct import or re-export namespace that exposes
+the public boundary and companion. Provider output must be generated and
+published before a dependent package is built. Missing permission, a hidden or
+ambiguous companion namespace, an unnameable signature, or dispatch that no
+longer reaches the exact storage slot is a generation error. A child must also
+validate the complete concrete hierarchy so field hiding below the boundary
+cannot redirect a generated getter or setter to another slot.
+
+Every non-ignored `final` or `late final` field must be initialized by a
+statically proven identity flow:
+
+```text
+selected concrete-child constructor parameter
+ -> redirect or super parameter
+ -> exact storage field
+```
+
+Accepted edges are exact field formals, super formals, direct parameter
+references in constructor field initializers, and direct parameter references
+in redirecting or super-constructor arguments. Types must remain identical
+after concrete generic substitution, including nullability. Calls, operators,
+casts, null assertions, constants, constructor-body assignment, and matching
+names without element identity are not proof. A declaration initializer on a
+non-ignored final field is unsupported because the decoded value cannot own
+that slot. There is no post-construction final write, reflection, or fallback.
+
+Mutable fields connected to selected constructor parameters are initialized
+once through the same exact identity flow. Remaining mutable fields require an
+exact setter and are restored after construction. Required constructor
+parameters must have one unambiguous field source. Optional named parameters
+may be omitted; an omitted optional positional parameter cannot be followed by
+a passed positional parameter. Constructor arguments and assignments are
+matched by resolved storage-field identity rather than field-name strings.
+
+The concrete child owns one `GeneratedStructSchema`, one canonical field sort,
+one serializer and descriptor cache, one reconstruction, one reference
+publication path, and one graph-memory owner. Parent serializers are neither
+nested nor invoked, and parent runtime registration is not required. A
+separately annotated concrete parent has its own independently flattened
+schema only for values of that exact type.
+
+Direct and inherited fields feed one normalized list into the existing
+recursive reference analysis and `needsRootRef` calculation. Inherited
+`ref: true` and nested container metadata behave like equivalent direct child
+fields. Inheritance adds no reference state, `ReadContext` API, serializer
+signature, call-contract change, runtime branch, slot, sentinel, callback,
+wrapper, compatible-layout state, or parent reference owner. A failure shared
+by the equivalent flat model is a separate reference-subsystem issue.
+
+Java provides only the flattened-model comparison for this feature: its object
+serializer extends one field-descriptor list across inheritance and adds no
+inheritance-specific reference state. Java's existing reference contract uses
+the `readRefIds` stack, real reference IDs, and its `-1` sentinel to associate
+`reference(obj)` with the current serializer invocation. That contract differs
+from Dart's current reference subsystem and is not a design to transplant as
+part of Dart hierarchy support. Dart inheritance is required only to match its
+own equivalent flat model.
+
+Hierarchy traversal, substitution, access validation, and constructor proofs
+run during generation. Existing flat serializers gain no runtime work, public
+and same-library inherited fields emit the same direct operations as equivalent
+flat fields, and cross-library private fields add only an inlineable typed
+static companion call. Generation must not introduce runtime hierarchy
+traversal, allocation, callbacks, reflection, or parent dispatch.
+
+The child's shallow graph-memory formula is:
+
+```text
+24 + 4 * actualConcreteStorageFieldCount
+```
+
+It counts every real inherited and ignored storage slot once and adds no parent
+self charge.
+
+Generated diagnostics must identify the concrete child, declaring field,
+declaring library, and failed access or constructor path, then give an
+actionable remedy. Typical remedies are adding
+`exposePrivateFields: true` to a public owner-library boundary, importing its
+generated companion, forwarding a constructor value unchanged, placing
+`@ForyField(ignore: true)` on the field declaration, or using a manual
+serializer. Diagnostics must state that hierarchy discovery is independent of
+cross-library access.
+
+This correction changes generated schemas for ordinary children that inherit
+storage. All affected `.fory.dart` files must be regenerated. Compatible mode
+uses normal missing/unknown-field handling; fixed schemas written while
+inherited fields were absent have no legacy reader. Implementations must not
+retain an alternative path limited to child declarations, constructor binding
+based only on names, a late-final setter path, compatibility shim, fallback
+access, or parent serializer delegation.
+
+Acceptance requires that every hierarchy storage slot is either explicitly
+ignored or represented exactly once with a valid type, access path, wire
+identity, and reconstruction path. Equivalent flat and inherited models must
+produce the same canonical schema, wire bytes, reference IDs, and round-trip
+behavior. External target declarations remain explicit and unaffected.
+
### Dart External Structural Serializers
Dart external-type serialization extends `ForyStruct` with an optional
@@ -1404,6 +1580,11 @@ reference-tracked paths back to the target are rejected during generation.
The recursive check includes target elements, keys, and values nested in the
supported list, set, and map field metadata.
+The external declaration's fields are the complete schema. It may explicitly
+name an accessible property inherited by the target, but the generator does
+not automatically scan the external target hierarchy for schema fields.
+`exposePrivateFields` is invalid on an external declaration.
+
An external object's shallow graph-memory formula is the union of declaration
fields and public instance fields discovered on the target, its superclasses,
and applied mixins. A public target field represented by a declaration is
@@ -1472,7 +1653,7 @@ For Dart implementation changes, run at minimum:
```bash
cd dart
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
dart analyze
dart test
```
@@ -1481,6 +1662,6 @@ For generated consumer coverage, also run:
```bash
cd dart/packages/fory-test
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
dart test
```
diff --git a/integration_tests/idl_tests/run_dart_tests.sh b/integration_tests/idl_tests/run_dart_tests.sh
index a35eb01c0..522b88f94 100755
--- a/integration_tests/idl_tests/run_dart_tests.sh
+++ b/integration_tests/idl_tests/run_dart_tests.sh
@@ -7,7 +7,7 @@ python "${SCRIPT_DIR}/generate_idl.py" --lang dart
cd "${SCRIPT_DIR}/dart"
dart pub get
-dart run build_runner build --delete-conflicting-outputs
+dart run build_runner build
dart test
IDL_PEER_LANG=dart "${SCRIPT_DIR}/run_java_tests.sh"
diff --git a/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java b/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java
index 85f058b03..f484a46da 100644
--- a/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/xlang/DartXlangTest.java
@@ -31,6 +31,7 @@ import lombok.Data;
import org.apache.fory.Fory;
import org.apache.fory.annotation.ForyField;
import org.apache.fory.annotation.ForyStruct;
+import org.apache.fory.annotation.Int32Type;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.memory.MemoryUtils;
import org.apache.fory.test.TestUtils;
@@ -45,19 +46,12 @@ public class DartXlangTest extends XlangTestBase {
private static final int DART_SETUP_TIMEOUT_SECONDS = 300;
private static final File DART_WORK_DIR = new File("../../dart");
private static final File DART_FORY_TEST_WORK_DIR = new File("../../dart/packages/fory-test");
- private static final File DART_XLANG_SOURCE_FILE =
- new File(DART_FORY_TEST_WORK_DIR, "lib/entity/xlang_test_models.dart");
- private static final File DART_XLANG_GENERATED_FILE =
- new File(DART_FORY_TEST_WORK_DIR, "lib/entity/xlang_test_models.fory.dart");
- private static final File DART_EXTERNAL_SOURCE_FILE =
- new File(DART_FORY_TEST_WORK_DIR, "lib/model/external_serializers.dart");
- private static final File DART_EXTERNAL_GENERATED_FILE =
- new File(DART_FORY_TEST_WORK_DIR, "lib/model/external_serializers.fory.dart");
+ private static final File DART_INHERITANCE_PROVIDER_WORK_DIR =
+ new File("../../dart/packages/inheritance-test-models");
private static final String DART_MODULE =
"packages/fory-test/test/cross_lang_test/xlang_test_main.dart";
private static final List<String> DART_CODEGEN_COMMAND =
- Arrays.asList(
- DART_EXECUTABLE, "run", "build_runner", "build", "--delete-conflicting-outputs");
+ Arrays.asList(DART_EXECUTABLE, "run", "build_runner", "build");
private static final List<String> DART_BASE_COMMAND =
Arrays.asList(DART_EXECUTABLE, "run", DART_MODULE, "<DART_TESTCASE>");
@@ -99,12 +93,14 @@ public class DartXlangTest extends XlangTestBase {
}
private static synchronized void ensureGeneratedXlangSpecs() {
- if (DART_XLANG_GENERATED_FILE.isFile()
- && DART_XLANG_GENERATED_FILE.lastModified() >= DART_XLANG_SOURCE_FILE.lastModified()
- && DART_EXTERNAL_GENERATED_FILE.isFile()
- && DART_EXTERNAL_GENERATED_FILE.lastModified()
- >= DART_EXTERNAL_SOURCE_FILE.lastModified()) {
- return;
+ if (!TestUtils.executeCommand(
+ DART_CODEGEN_COMMAND,
+ DART_SETUP_TIMEOUT_SECONDS,
+ Collections.emptyMap(),
+ DART_INHERITANCE_PROVIDER_WORK_DIR)) {
+ throw new IllegalStateException(
+ "Failed to generate Dart inheritance providers by command: "
+ + String.join(" ", DART_CODEGEN_COMMAND));
}
if (!TestUtils.executeCommand(
DART_CODEGEN_COMMAND,
@@ -131,6 +127,30 @@ public class DartXlangTest extends XlangTestBase {
int age;
}
+ @Data
+ @ForyStruct
+ static class FlatInheritedStruct {
+ @ForyField(id = 1)
+ @Int32Type
+ int childInt;
+
+ @ForyField(id = 2)
+ @Int32Type
+ int parentInt;
+
+ @ForyField(id = 5)
+ boolean childFlag;
+
+ @ForyField(id = 6)
+ boolean parentFlag;
+
+ @ForyField(id = 9)
+ String childText;
+
+ @ForyField(id = 10)
+ String parentText;
+ }
+
private void runExternalUser(boolean enableCodegen, boolean named) throws IOException {
Fory fory =
Fory.builder().withXlang(true).withCompatible(true).withCodegen(enableCodegen).build();
@@ -163,6 +183,43 @@ public class DartXlangTest extends XlangTestBase {
runExternalUser(enableCodegen, true);
}
+ @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+ public void testInheritedFlatStruct(boolean enableCodegen) throws IOException {
+ Fory fory =
+ Fory.builder()
+ .withXlang(true)
+ .withCompatible(false)
+ .withCodegen(enableCodegen)
+ .withMetaCompressor(new NoOpMetaCompressor())
+ .build();
+ fory.register(FlatInheritedStruct.class, 1101);
+
+ FlatInheritedStruct expected = new FlatInheritedStruct();
+ expected.childInt = 101;
+ expected.parentInt = 202;
+ expected.childFlag = true;
+ expected.parentFlag = false;
+ expected.childText = "child";
+ expected.parentText = "parent";
+ byte[] javaBytes = fory.serialize(expected);
+
+ ExecutionContext javaContext = prepareExecution("test_inherited_flat_from_java", javaBytes);
+ runPeer(javaContext);
+ byte[] dartRoundTripBytes = readBytes(javaContext.dataFile());
+ Assert.assertTrue(
+ Arrays.equals(dartRoundTripBytes, javaBytes),
+ "Dart inherited round-trip must preserve Java flat bytes.");
+ Assert.assertEquals(fory.deserialize(dartRoundTripBytes), expected);
+
+ ExecutionContext dartContext = prepareExecution("test_inherited_flat_from_dart", new byte[0]);
+ runPeer(dartContext);
+ byte[] dartBytes = readBytes(dartContext.dataFile());
+ Assert.assertTrue(
+ Arrays.equals(dartBytes, javaBytes),
+ "Dart inherited and Java flat writers must emit identical bytes.");
+ Assert.assertEquals(fory.deserialize(dartBytes), expected);
+ }
+
@Test(groups = "xlang")
public void testBuffer() throws java.io.IOException {
super.testBuffer();
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org