mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-24 13:47:24 +00:00
Compare commits
3 Commits
cherrypick
...
cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76c77b1f6d | ||
|
|
edc2a14ad7 | ||
|
|
71d3ef77a9 |
201
cherrypick_cli/LICENSE
Normal file
201
cherrypick_cli/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed 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.
|
||||||
93
cherrypick_cli/README.md
Normal file
93
cherrypick_cli/README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# CherryPick CLI
|
||||||
|
|
||||||
|
A command-line tool for managing and generating `build.yaml` configuration for the [CherryPick](https://github.com/pese-git/cherrypick) dependency injection ecosystem for Dart & Flutter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- 📦 Quickly add or update CherryPick generator sections in your project's `build.yaml`.
|
||||||
|
- 🛡️ Safely preserves unrelated configs and packages.
|
||||||
|
- 📝 Always outputs a human-friendly, formatted YAML file.
|
||||||
|
- 🏷️ Supports custom output directories and custom build.yaml file paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. **Navigate to the CLI package directory:**
|
||||||
|
```bash
|
||||||
|
cd cherrypick_cli
|
||||||
|
```
|
||||||
|
2. **Get dependencies:**
|
||||||
|
```bash
|
||||||
|
dart pub get
|
||||||
|
```
|
||||||
|
3. **Run the CLI:**
|
||||||
|
```bash
|
||||||
|
dart run cherrypick_cli init --output_dir=lib/generated
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Show help
|
||||||
|
```bash
|
||||||
|
dart run cherrypick_cli --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add or update CherryPick sections in build.yaml
|
||||||
|
```bash
|
||||||
|
dart run cherrypick_cli init --output_dir=lib/generated
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options:
|
||||||
|
- `--output_dir`, `-o` — Directory for generated code (default: `lib/generated`)
|
||||||
|
- `--build_yaml`, `-f` — Path to build.yaml file (default: `build.yaml`)
|
||||||
|
|
||||||
|
#### Example with custom build.yaml
|
||||||
|
```bash
|
||||||
|
dart run cherrypick_cli init --output_dir=custom/dir --build_yaml=custom_build.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What does it do?
|
||||||
|
- Adds or updates the following sections in your `build.yaml` (or custom file):
|
||||||
|
- `cherrypick_generator|inject_generator`
|
||||||
|
- `cherrypick_generator|module_generator`
|
||||||
|
- Ensures all YAML is pretty-printed and readable.
|
||||||
|
- Leaves unrelated configs untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
```yaml
|
||||||
|
targets:
|
||||||
|
$default:
|
||||||
|
builders:
|
||||||
|
cherrypick_generator|inject_generator:
|
||||||
|
options:
|
||||||
|
build_extensions:
|
||||||
|
^lib/{{}}.dart:
|
||||||
|
- lib/generated/{{}}.inject.cherrypick.g.dart
|
||||||
|
output_dir: lib/generated
|
||||||
|
generate_for:
|
||||||
|
- lib/**.dart
|
||||||
|
cherrypick_generator|module_generator:
|
||||||
|
options:
|
||||||
|
build_extensions:
|
||||||
|
^lib/di/{{}}.dart:
|
||||||
|
- lib/generated/di/{{}}.module.cherrypick.g.dart
|
||||||
|
output_dir: lib/generated
|
||||||
|
generate_for:
|
||||||
|
- lib/**.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Pull requests and issues are welcome! See the [main CherryPick repo](https://github.com/pese-git/cherrypick) for more.
|
||||||
|
|
||||||
|
## License
|
||||||
|
See [LICENSE](LICENSE).
|
||||||
8
cherrypick_cli/bin/cherrypick_cli.dart
Normal file
8
cherrypick_cli/bin/cherrypick_cli.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:cherrypick_cli/src/commands/init_command.dart';
|
||||||
|
|
||||||
|
void main(List<String> args) {
|
||||||
|
final runner = CommandRunner('cherrypick_cli', 'CherryPick CLI')
|
||||||
|
..addCommand(InitCommand());
|
||||||
|
runner.run(args);
|
||||||
|
}
|
||||||
34
cherrypick_cli/lib/src/commands/init_command.dart
Normal file
34
cherrypick_cli/lib/src/commands/init_command.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import '../utils/build_yaml_updater.dart';
|
||||||
|
|
||||||
|
class InitCommand extends Command {
|
||||||
|
@override
|
||||||
|
final name = 'init';
|
||||||
|
@override
|
||||||
|
final description = 'Adds or updates cherrypick_generator sections in build.yaml, preserving other packages.';
|
||||||
|
|
||||||
|
InitCommand() {
|
||||||
|
argParser.addOption(
|
||||||
|
'output_dir',
|
||||||
|
abbr: 'o',
|
||||||
|
defaultsTo: 'lib/generated',
|
||||||
|
help: 'Directory for generated code.',
|
||||||
|
);
|
||||||
|
argParser.addOption(
|
||||||
|
'build_yaml',
|
||||||
|
abbr: 'f',
|
||||||
|
defaultsTo: 'build.yaml',
|
||||||
|
help: 'Path to build.yaml file.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void run() {
|
||||||
|
final outputDir = argResults?['output_dir'] as String? ?? 'lib/generated';
|
||||||
|
final buildYaml = argResults?['build_yaml'] as String? ?? 'build.yaml';
|
||||||
|
updateCherrypickBuildYaml(
|
||||||
|
buildYamlPath: buildYaml,
|
||||||
|
outputDir: outputDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
cherrypick_cli/lib/src/utils/build_yaml_updater.dart
Normal file
76
cherrypick_cli/lib/src/utils/build_yaml_updater.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
import 'package:json2yaml/json2yaml.dart';
|
||||||
|
|
||||||
|
void updateCherrypickBuildYaml({
|
||||||
|
String buildYamlPath = 'build.yaml',
|
||||||
|
String outputDir = 'lib/generated',
|
||||||
|
}) {
|
||||||
|
final file = File(buildYamlPath);
|
||||||
|
final exists = file.existsSync();
|
||||||
|
Map config = {};
|
||||||
|
if (exists) {
|
||||||
|
final content = file.readAsStringSync();
|
||||||
|
final loaded = loadYaml(content);
|
||||||
|
config = _deepYamlToMap(loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Гарантируем вложенность
|
||||||
|
config['targets'] ??= {};
|
||||||
|
final targets = config['targets'] as Map;
|
||||||
|
targets['\$default'] ??= {};
|
||||||
|
final def = targets['\$default'] as Map;
|
||||||
|
def['builders'] ??= {};
|
||||||
|
final builders = def['builders'] as Map;
|
||||||
|
|
||||||
|
builders['cherrypick_generator|inject_generator'] = {
|
||||||
|
'options': {
|
||||||
|
'build_extensions': {
|
||||||
|
'^lib/{{}}.dart': ['${outputDir}/{{}}.inject.cherrypick.g.dart']
|
||||||
|
},
|
||||||
|
'output_dir': outputDir
|
||||||
|
},
|
||||||
|
'generate_for': ['lib/**.dart']
|
||||||
|
};
|
||||||
|
|
||||||
|
builders['cherrypick_generator|module_generator'] = {
|
||||||
|
'options': {
|
||||||
|
'build_extensions': {
|
||||||
|
'^lib/di/{{}}.dart': ['${outputDir}/di/{{}}.module.cherrypick.g.dart']
|
||||||
|
},
|
||||||
|
'output_dir': outputDir
|
||||||
|
},
|
||||||
|
'generate_for': ['lib/**.dart']
|
||||||
|
};
|
||||||
|
|
||||||
|
final yamlString = json2yaml(_stringifyKeys(config), yamlStyle: YamlStyle.pubspecYaml);
|
||||||
|
file.writeAsStringSync(yamlString);
|
||||||
|
print('✅ build.yaml has been successfully updated and formatted (cherrypick sections added/updated).');
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic _stringifyKeys(dynamic node) {
|
||||||
|
if (node is Map) {
|
||||||
|
return Map.fromEntries(
|
||||||
|
node.entries.map(
|
||||||
|
(e) => MapEntry(e.key.toString(), _stringifyKeys(e.value)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (node is List) {
|
||||||
|
return node.map(_stringifyKeys).toList();
|
||||||
|
} else {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Рекурсивно преобразует YamlMap/YamlList в обычные Map/List
|
||||||
|
dynamic _deepYamlToMap(dynamic node) {
|
||||||
|
if (node is YamlMap) {
|
||||||
|
return Map.fromEntries(node.entries.map((e) => MapEntry(e.key, _deepYamlToMap(e.value))));
|
||||||
|
} else if (node is YamlList) {
|
||||||
|
return node.map(_deepYamlToMap).toList();
|
||||||
|
} else {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
85
cherrypick_cli/pubspec.lock
Normal file
85
cherrypick_cli/pubspec.lock
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
args:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
json2yaml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: json2yaml
|
||||||
|
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.17.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.1"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
yaml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
yaml_edit:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: yaml_edit
|
||||||
|
sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.5.0 <4.0.0"
|
||||||
13
cherrypick_cli/pubspec.yaml
Normal file
13
cherrypick_cli/pubspec.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: cherrypick_cli
|
||||||
|
version: 0.1.0
|
||||||
|
publish_to: none
|
||||||
|
description: CLI tool for CherryPick DI ecosystem
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
dependencies:
|
||||||
|
args: ^2.4.2
|
||||||
|
yaml: ^3.1.2
|
||||||
|
yaml_edit: ^2.1.1
|
||||||
|
json2yaml: ^3.0.0
|
||||||
|
executables:
|
||||||
|
cherrypick_cli:
|
||||||
@@ -56,9 +56,43 @@ class CustomOutputBuilder extends Builder {
|
|||||||
final outputId = AssetId(inputId.package, outputPath);
|
final outputId = AssetId(inputId.package, outputPath);
|
||||||
// part of - всегда авто!
|
// part of - всегда авто!
|
||||||
final partOfPath = p.relative(inputId.path, from: p.dirname(outputPath));
|
final partOfPath = p.relative(inputId.path, from: p.dirname(outputPath));
|
||||||
final codeWithPartOf = "part of '$partOfPath';\n\n$generated";
|
|
||||||
|
// Check if generated code starts with formatting header
|
||||||
|
String finalCode;
|
||||||
|
if (generated.startsWith('// dart format width=80')) {
|
||||||
|
// Find the end of the header (after "// GENERATED CODE - DO NOT MODIFY BY HAND")
|
||||||
|
final lines = generated.split('\n');
|
||||||
|
int headerEndIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].startsWith('// GENERATED CODE - DO NOT MODIFY BY HAND')) {
|
||||||
|
headerEndIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerEndIndex != -1) {
|
||||||
|
// Insert part of directive after the header
|
||||||
|
final headerLines = lines.sublist(0, headerEndIndex + 1);
|
||||||
|
final remainingLines = lines.sublist(headerEndIndex + 1);
|
||||||
|
|
||||||
|
final headerPart = headerLines.join('\n');
|
||||||
|
final remainingPart = remainingLines.join('\n');
|
||||||
|
|
||||||
|
// Preserve trailing newline if original had one
|
||||||
|
final hasTrailingNewline = generated.endsWith('\n');
|
||||||
|
finalCode = '$headerPart\n\npart of \'$partOfPath\';\n$remainingPart${hasTrailingNewline ? '' : '\n'}';
|
||||||
|
} else {
|
||||||
|
// Fallback: add part of at the beginning
|
||||||
|
finalCode = "part of '$partOfPath';\n\n$generated";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No header, add part of at the beginning
|
||||||
|
finalCode = "part of '$partOfPath';\n\n$generated";
|
||||||
|
}
|
||||||
|
|
||||||
print('[CustomOutputBuilder] writing to output: \\${outputId.path}');
|
print('[CustomOutputBuilder] writing to output: \\${outputId.path}');
|
||||||
await buildStep.writeAsString(outputId, codeWithPartOf);
|
await buildStep.writeAsString(outputId, finalCode);
|
||||||
print('[CustomOutputBuilder] successfully written for input: \\${inputId.path}');
|
print('[CustomOutputBuilder] successfully written for input: \\${inputId.path}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,16 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:analyzer/dart/constant/value.dart';
|
import 'package:analyzer/dart/constant/value.dart';
|
||||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
|
||||||
import 'package:build/build.dart';
|
import 'package:build/build.dart';
|
||||||
import 'package:source_gen/source_gen.dart';
|
import 'package:source_gen/source_gen.dart';
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
|
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
|
||||||
import 'cherrypick_custom_builders.dart' as custom;
|
import 'cherrypick_custom_builders.dart' as custom;
|
||||||
|
import 'src/exceptions.dart';
|
||||||
|
import 'src/type_parser.dart';
|
||||||
|
import 'src/annotation_validator.dart';
|
||||||
|
|
||||||
/// InjectGenerator generates a mixin for a class marked with @injectable()
|
/// InjectGenerator generates a mixin for a class marked with @injectable()
|
||||||
/// and injects all fields annotated with @inject(), using CherryPick DI.
|
/// and injects all fields annotated with @inject(), using CherryPick DI.
|
||||||
@@ -50,34 +53,68 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
|||||||
BuildStep buildStep,
|
BuildStep buildStep,
|
||||||
) {
|
) {
|
||||||
if (element is! ClassElement) {
|
if (element is! ClassElement) {
|
||||||
throw InvalidGenerationSourceError(
|
throw CherryPickGeneratorException(
|
||||||
'@injectable() can only be applied to classes.',
|
'@injectable() can only be applied to classes',
|
||||||
element: element,
|
element: element,
|
||||||
|
category: 'INVALID_TARGET',
|
||||||
|
suggestion: 'Apply @injectable() to a class instead of ${element.runtimeType}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final classElement = element;
|
final classElement = element;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate class annotations
|
||||||
|
AnnotationValidator.validateClassAnnotations(classElement);
|
||||||
|
|
||||||
|
return _generateInjectionCode(classElement);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is CherryPickGeneratorException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw CodeGenerationException(
|
||||||
|
'Failed to generate injection code: $e',
|
||||||
|
element: classElement,
|
||||||
|
suggestion: 'Check that all @inject fields have valid types and annotations',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the injection code for a class
|
||||||
|
String _generateInjectionCode(ClassElement classElement) {
|
||||||
final className = classElement.name;
|
final className = classElement.name;
|
||||||
final mixinName = '_\$$className';
|
final mixinName = '_\$$className';
|
||||||
|
|
||||||
|
// Collect and process all @inject fields.
|
||||||
|
final injectFields = classElement.fields
|
||||||
|
.where(_isInjectField)
|
||||||
|
.map((field) => _parseInjectField(field, classElement))
|
||||||
|
.toList();
|
||||||
|
|
||||||
final buffer = StringBuffer()
|
final buffer = StringBuffer()
|
||||||
..writeln('mixin $mixinName {')
|
..writeln('// dart format width=80')
|
||||||
..writeln(' void _inject($className instance) {');
|
..writeln('// GENERATED CODE - DO NOT MODIFY BY HAND')
|
||||||
|
..writeln()
|
||||||
|
..writeln('// **************************************************************************')
|
||||||
|
..writeln('// InjectGenerator')
|
||||||
|
..writeln('// **************************************************************************')
|
||||||
|
..writeln()
|
||||||
|
..writeln('mixin $mixinName {');
|
||||||
|
|
||||||
// Collect and process all @inject fields.
|
if (injectFields.isEmpty) {
|
||||||
// Собираем и обрабатываем все поля с @inject.
|
// For empty classes, generate a method with empty body
|
||||||
final injectFields =
|
buffer.writeln(' void _inject($className instance) {}');
|
||||||
classElement.fields.where(_isInjectField).map(_parseInjectField);
|
} else {
|
||||||
|
buffer.writeln(' void _inject($className instance) {');
|
||||||
for (final parsedField in injectFields) {
|
for (final parsedField in injectFields) {
|
||||||
buffer.writeln(_generateInjectionLine(parsedField));
|
buffer.writeln(_generateInjectionLine(parsedField));
|
||||||
|
}
|
||||||
|
buffer.writeln(' }');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer.writeln('}');
|
||||||
|
|
||||||
buffer
|
return '${buffer.toString()}\n';
|
||||||
..writeln(' }')
|
|
||||||
..writeln('}');
|
|
||||||
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a field has the @inject annotation.
|
/// Checks if a field has the @inject annotation.
|
||||||
@@ -94,50 +131,51 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
|||||||
///
|
///
|
||||||
/// Разбирает поле на наличие модификаторов scope/named и выясняет его тип.
|
/// Разбирает поле на наличие модификаторов scope/named и выясняет его тип.
|
||||||
/// Возвращает [_ParsedInjectField] с информацией о внедрении.
|
/// Возвращает [_ParsedInjectField] с информацией о внедрении.
|
||||||
static _ParsedInjectField _parseInjectField(FieldElement field) {
|
static _ParsedInjectField _parseInjectField(FieldElement field, ClassElement classElement) {
|
||||||
String? scopeName;
|
try {
|
||||||
String? namedValue;
|
// Validate field annotations
|
||||||
|
AnnotationValidator.validateFieldAnnotations(field);
|
||||||
|
|
||||||
|
// Parse type using improved type parser
|
||||||
|
final parsedType = TypeParser.parseType(field.type, field);
|
||||||
|
TypeParser.validateInjectableType(parsedType, field);
|
||||||
|
|
||||||
|
// Extract metadata
|
||||||
|
String? scopeName;
|
||||||
|
String? namedValue;
|
||||||
|
|
||||||
for (final meta in field.metadata) {
|
for (final meta in field.metadata) {
|
||||||
final DartObject? obj = meta.computeConstantValue();
|
final DartObject? obj = meta.computeConstantValue();
|
||||||
final type = obj?.type?.getDisplayString();
|
final type = obj?.type?.getDisplayString();
|
||||||
if (type == 'scope') {
|
if (type == 'scope') {
|
||||||
scopeName = obj?.getField('name')?.toStringValue();
|
scopeName = obj?.getField('name')?.toStringValue();
|
||||||
} else if (type == 'named') {
|
} else if (type == 'named') {
|
||||||
namedValue = obj?.getField('value')?.toStringValue();
|
namedValue = obj?.getField('value')?.toStringValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _ParsedInjectField(
|
||||||
|
fieldName: field.name,
|
||||||
|
parsedType: parsedType,
|
||||||
|
scopeName: scopeName,
|
||||||
|
namedValue: namedValue,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is CherryPickGeneratorException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw DependencyResolutionException(
|
||||||
|
'Failed to parse inject field "${field.name}"',
|
||||||
|
element: field,
|
||||||
|
suggestion: 'Check that the field type is valid and properly imported',
|
||||||
|
context: {
|
||||||
|
'field_name': field.name,
|
||||||
|
'field_type': field.type.getDisplayString(),
|
||||||
|
'class_name': classElement.name,
|
||||||
|
'error': e.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final DartType dartType = field.type;
|
|
||||||
String coreTypeName;
|
|
||||||
bool isFuture;
|
|
||||||
|
|
||||||
if (dartType.isDartAsyncFuture) {
|
|
||||||
final ParameterizedType paramType = dartType as ParameterizedType;
|
|
||||||
coreTypeName = paramType.typeArguments.first.getDisplayString();
|
|
||||||
isFuture = true;
|
|
||||||
} else {
|
|
||||||
coreTypeName = dartType.getDisplayString();
|
|
||||||
isFuture = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ***
|
|
||||||
// Добавим определение nullable для типа (например PostRepository? или Future<PostRepository?>)
|
|
||||||
bool isNullable = dartType.nullabilitySuffix ==
|
|
||||||
NullabilitySuffix.question ||
|
|
||||||
(dartType is ParameterizedType &&
|
|
||||||
(dartType)
|
|
||||||
.typeArguments
|
|
||||||
.any((t) => t.nullabilitySuffix == NullabilitySuffix.question));
|
|
||||||
|
|
||||||
return _ParsedInjectField(
|
|
||||||
fieldName: field.name,
|
|
||||||
coreType: coreTypeName.replaceAll('?', ''), // удаляем "?" на всякий
|
|
||||||
isFuture: isFuture,
|
|
||||||
isNullable: isNullable,
|
|
||||||
scopeName: scopeName,
|
|
||||||
namedValue: namedValue,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a line of code that performs the dependency injection for a field.
|
/// Generates a line of code that performs the dependency injection for a field.
|
||||||
@@ -146,24 +184,47 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
|||||||
/// Генерирует строку кода, которая внедряет зависимость для поля.
|
/// Генерирует строку кода, которая внедряет зависимость для поля.
|
||||||
/// Учитывает resolve/resolveAsync, scoping и named qualifier.
|
/// Учитывает resolve/resolveAsync, scoping и named qualifier.
|
||||||
String _generateInjectionLine(_ParsedInjectField field) {
|
String _generateInjectionLine(_ParsedInjectField field) {
|
||||||
// Используем tryResolve для nullable, иначе resolve
|
final resolveMethod = '${field.parsedType.resolveMethodName}<${field.parsedType.codeGenType}>';
|
||||||
final resolveMethod = field.isFuture
|
final fieldName = field.fieldName;
|
||||||
? (field.isNullable
|
|
||||||
? 'tryResolveAsync<${field.coreType}>'
|
// Build the scope call
|
||||||
: 'resolveAsync<${field.coreType}>')
|
|
||||||
: (field.isNullable
|
|
||||||
? 'tryResolve<${field.coreType}>'
|
|
||||||
: 'resolve<${field.coreType}>');
|
|
||||||
|
|
||||||
final openCall = (field.scopeName != null && field.scopeName!.isNotEmpty)
|
final openCall = (field.scopeName != null && field.scopeName!.isNotEmpty)
|
||||||
? "CherryPick.openScope(scopeName: '${field.scopeName}')"
|
? "CherryPick.openScope(scopeName: '${field.scopeName}')"
|
||||||
: "CherryPick.openRootScope()";
|
: "CherryPick.openRootScope()";
|
||||||
|
|
||||||
final params = (field.namedValue != null && field.namedValue!.isNotEmpty)
|
// Build the parameters
|
||||||
? "(named: '${field.namedValue}')"
|
final hasNamedParam = field.namedValue != null && field.namedValue!.isNotEmpty;
|
||||||
: '()';
|
final params = hasNamedParam ? "(named: '${field.namedValue}')" : '()';
|
||||||
|
|
||||||
return " instance.${field.fieldName} = $openCall.$resolveMethod$params;";
|
// Create the full line
|
||||||
|
final fullLine = " instance.$fieldName = $openCall.$resolveMethod$params;";
|
||||||
|
|
||||||
|
// Check if line is too long (dart format width=80, accounting for indentation)
|
||||||
|
if (fullLine.length <= 80) {
|
||||||
|
return fullLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format long lines with proper line breaks
|
||||||
|
if (hasNamedParam && field.scopeName != null && field.scopeName!.isNotEmpty) {
|
||||||
|
// For scoped calls with named parameters, break after openScope
|
||||||
|
return " instance.$fieldName = CherryPick.openScope(\n"
|
||||||
|
" scopeName: '${field.scopeName}',\n"
|
||||||
|
" ).$resolveMethod(named: '${field.namedValue}');";
|
||||||
|
} else if (hasNamedParam) {
|
||||||
|
// For named parameters without scope, break after the method call
|
||||||
|
return " instance.$fieldName = $openCall.$resolveMethod(\n"
|
||||||
|
" named: '${field.namedValue}',\n"
|
||||||
|
" );";
|
||||||
|
} else if (field.scopeName != null && field.scopeName!.isNotEmpty) {
|
||||||
|
// For scoped calls without named params, break after openScope with proper parameter formatting
|
||||||
|
return " instance.$fieldName = CherryPick.openScope(\n"
|
||||||
|
" scopeName: '${field.scopeName}',\n"
|
||||||
|
" ).$resolveMethod();";
|
||||||
|
} else {
|
||||||
|
// For simple long calls, break after openRootScope
|
||||||
|
return " instance.$fieldName = $openCall\n"
|
||||||
|
" .$resolveMethod();";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,12 +237,8 @@ class _ParsedInjectField {
|
|||||||
/// The name of the field / Имя поля.
|
/// The name of the field / Имя поля.
|
||||||
final String fieldName;
|
final String fieldName;
|
||||||
|
|
||||||
/// The base type name (T or Future<T>) / Базовый тип (T или тип из Future<T>).
|
/// Parsed type information / Информация о типе поля.
|
||||||
final String coreType;
|
final ParsedType parsedType;
|
||||||
|
|
||||||
/// True if the field type is Future<T>; false otherwise
|
|
||||||
/// Истина, если поле — Future<T>, иначе — ложь.
|
|
||||||
final bool isFuture;
|
|
||||||
|
|
||||||
/// Optional scope annotation argument / Опциональное имя scope.
|
/// Optional scope annotation argument / Опциональное имя scope.
|
||||||
final String? scopeName;
|
final String? scopeName;
|
||||||
@@ -189,16 +246,18 @@ class _ParsedInjectField {
|
|||||||
/// Optional named annotation argument / Опциональное имя named.
|
/// Optional named annotation argument / Опциональное имя named.
|
||||||
final String? namedValue;
|
final String? namedValue;
|
||||||
|
|
||||||
final bool isNullable;
|
|
||||||
|
|
||||||
_ParsedInjectField({
|
_ParsedInjectField({
|
||||||
required this.fieldName,
|
required this.fieldName,
|
||||||
required this.coreType,
|
required this.parsedType,
|
||||||
required this.isFuture,
|
|
||||||
required this.isNullable,
|
|
||||||
this.scopeName,
|
this.scopeName,
|
||||||
this.namedValue,
|
this.namedValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '_ParsedInjectField(fieldName: $fieldName, parsedType: $parsedType, '
|
||||||
|
'scopeName: $scopeName, namedValue: $namedValue)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder factory. Used by build_runner.
|
/// Builder factory. Used by build_runner.
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import 'package:build/build.dart';
|
|||||||
import 'package:source_gen/source_gen.dart';
|
import 'package:source_gen/source_gen.dart';
|
||||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
|
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
|
||||||
import 'src/generated_class.dart';
|
import 'src/generated_class.dart';
|
||||||
|
import 'src/exceptions.dart';
|
||||||
|
import 'src/annotation_validator.dart';
|
||||||
import 'cherrypick_custom_builders.dart' as custom;
|
import 'cherrypick_custom_builders.dart' as custom;
|
||||||
/// ---------------------------------------------------------------------------
|
/// ---------------------------------------------------------------------------
|
||||||
/// ModuleGenerator for code generation of dependency-injected modules.
|
/// ModuleGenerator for code generation of dependency-injected modules.
|
||||||
@@ -61,20 +63,40 @@ class ModuleGenerator extends GeneratorForAnnotation<ann.module> {
|
|||||||
// Only classes are supported for @module() annotation
|
// Only classes are supported for @module() annotation
|
||||||
// Обрабатываются только классы (другие элементы — ошибка)
|
// Обрабатываются только классы (другие элементы — ошибка)
|
||||||
if (element is! ClassElement) {
|
if (element is! ClassElement) {
|
||||||
throw InvalidGenerationSourceError(
|
throw CherryPickGeneratorException(
|
||||||
'@module() can only be applied to classes. / @module() может быть применён только к классам.',
|
'@module() can only be applied to classes',
|
||||||
element: element,
|
element: element,
|
||||||
|
category: 'INVALID_TARGET',
|
||||||
|
suggestion: 'Apply @module() to a class instead of ${element.runtimeType}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final classElement = element;
|
final classElement = element;
|
||||||
|
|
||||||
// Build a representation of the generated bindings based on class methods /
|
try {
|
||||||
// Создаёт объект, описывающий, какие биндинги нужно сгенерировать на основании методов класса
|
// Validate class annotations
|
||||||
final generatedClass = GeneratedClass.fromClassElement(classElement);
|
AnnotationValidator.validateClassAnnotations(classElement);
|
||||||
|
|
||||||
|
// Build a representation of the generated bindings based on class methods
|
||||||
|
final generatedClass = GeneratedClass.fromClassElement(classElement);
|
||||||
|
|
||||||
// Generate the resulting Dart code / Генерирует итоговый Dart-код
|
// Generate the resulting Dart code
|
||||||
return generatedClass.generate();
|
return generatedClass.generate();
|
||||||
|
} catch (e) {
|
||||||
|
if (e is CherryPickGeneratorException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw CodeGenerationException(
|
||||||
|
'Failed to generate module code for class "${classElement.name}"',
|
||||||
|
element: classElement,
|
||||||
|
suggestion: 'Check that all methods have valid @instance or @provide annotations',
|
||||||
|
context: {
|
||||||
|
'class_name': classElement.name,
|
||||||
|
'method_count': classElement.methods.length,
|
||||||
|
'error': e.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
314
cherrypick_generator/lib/src/annotation_validator.dart
Normal file
314
cherrypick_generator/lib/src/annotation_validator.dart
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||||
|
// Licensed 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:analyzer/dart/element/element.dart';
|
||||||
|
import 'exceptions.dart';
|
||||||
|
import 'metadata_utils.dart';
|
||||||
|
|
||||||
|
/// Validates annotation combinations and usage patterns
|
||||||
|
class AnnotationValidator {
|
||||||
|
/// Validates annotations on a method element
|
||||||
|
static void validateMethodAnnotations(MethodElement method) {
|
||||||
|
final annotations = _getAnnotationNames(method.metadata);
|
||||||
|
|
||||||
|
_validateMutuallyExclusiveAnnotations(method, annotations);
|
||||||
|
_validateAnnotationCombinations(method, annotations);
|
||||||
|
_validateAnnotationParameters(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates annotations on a field element
|
||||||
|
static void validateFieldAnnotations(FieldElement field) {
|
||||||
|
final annotations = _getAnnotationNames(field.metadata);
|
||||||
|
|
||||||
|
_validateInjectFieldAnnotations(field, annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates annotations on a class element
|
||||||
|
static void validateClassAnnotations(ClassElement classElement) {
|
||||||
|
final annotations = _getAnnotationNames(classElement.metadata);
|
||||||
|
|
||||||
|
_validateModuleClassAnnotations(classElement, annotations);
|
||||||
|
_validateInjectableClassAnnotations(classElement, annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> _getAnnotationNames(List<ElementAnnotation> metadata) {
|
||||||
|
return metadata
|
||||||
|
.map((m) => m.computeConstantValue()?.type?.getDisplayString())
|
||||||
|
.where((name) => name != null)
|
||||||
|
.cast<String>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateMutuallyExclusiveAnnotations(
|
||||||
|
MethodElement method,
|
||||||
|
List<String> annotations,
|
||||||
|
) {
|
||||||
|
// @instance and @provide are mutually exclusive
|
||||||
|
if (annotations.contains('instance') && annotations.contains('provide')) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'Method cannot have both @instance and @provide annotations',
|
||||||
|
element: method,
|
||||||
|
suggestion: 'Use either @instance for direct instances or @provide for factory methods',
|
||||||
|
context: {
|
||||||
|
'method_name': method.displayName,
|
||||||
|
'annotations': annotations,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateAnnotationCombinations(
|
||||||
|
MethodElement method,
|
||||||
|
List<String> annotations,
|
||||||
|
) {
|
||||||
|
// @params can only be used with @provide
|
||||||
|
if (annotations.contains('params') && !annotations.contains('provide')) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'@params annotation can only be used with @provide annotation',
|
||||||
|
element: method,
|
||||||
|
suggestion: 'Remove @params or add @provide annotation',
|
||||||
|
context: {
|
||||||
|
'method_name': method.displayName,
|
||||||
|
'annotations': annotations,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods must have either @instance or @provide
|
||||||
|
if (!annotations.contains('instance') && !annotations.contains('provide')) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'Method must be marked with either @instance or @provide annotation',
|
||||||
|
element: method,
|
||||||
|
suggestion: 'Add @instance() for direct instances or @provide() for factory methods',
|
||||||
|
context: {
|
||||||
|
'method_name': method.displayName,
|
||||||
|
'available_annotations': annotations,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @singleton validation
|
||||||
|
if (annotations.contains('singleton')) {
|
||||||
|
_validateSingletonUsage(method, annotations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateSingletonUsage(
|
||||||
|
MethodElement method,
|
||||||
|
List<String> annotations,
|
||||||
|
) {
|
||||||
|
// Singleton with params might not make sense in some contexts
|
||||||
|
if (annotations.contains('params')) {
|
||||||
|
// This is a warning, not an error - could be useful for parameterized singletons
|
||||||
|
// We could add a warning system later
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if return type is suitable for singleton
|
||||||
|
final returnType = method.returnType.getDisplayString();
|
||||||
|
if (returnType == 'void') {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'Singleton methods cannot return void',
|
||||||
|
element: method,
|
||||||
|
suggestion: 'Remove @singleton annotation or change return type',
|
||||||
|
context: {
|
||||||
|
'method_name': method.displayName,
|
||||||
|
'return_type': returnType,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateAnnotationParameters(MethodElement method) {
|
||||||
|
// Validate @named annotation parameters
|
||||||
|
final namedValue = MetadataUtils.getNamedValue(method.metadata);
|
||||||
|
if (namedValue != null) {
|
||||||
|
if (namedValue.isEmpty) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'@named annotation cannot have empty value',
|
||||||
|
element: method,
|
||||||
|
suggestion: 'Provide a non-empty string value for @named annotation',
|
||||||
|
context: {
|
||||||
|
'method_name': method.displayName,
|
||||||
|
'named_value': namedValue,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid naming conventions
|
||||||
|
if (!RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$').hasMatch(namedValue)) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'@named value should follow valid identifier naming conventions',
|
||||||
|
element: method,
|
||||||
|
suggestion: 'Use alphanumeric characters and underscores only, starting with a letter or underscore',
|
||||||
|
context: {
|
||||||
|
'method_name': method.displayName,
|
||||||
|
'named_value': namedValue,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate method parameters for @params usage
|
||||||
|
for (final param in method.parameters) {
|
||||||
|
final paramAnnotations = _getAnnotationNames(param.metadata);
|
||||||
|
if (paramAnnotations.contains('params')) {
|
||||||
|
_validateParamsParameter(param, method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateParamsParameter(ParameterElement param, MethodElement method) {
|
||||||
|
// @params parameter should typically be dynamic or Map<String, dynamic>
|
||||||
|
final paramType = param.type.getDisplayString();
|
||||||
|
|
||||||
|
if (paramType != 'dynamic' &&
|
||||||
|
paramType != 'Map<String, dynamic>' &&
|
||||||
|
paramType != 'Map<String, dynamic>?') {
|
||||||
|
// This is more of a warning - other types might be valid
|
||||||
|
// We could add a warning system for this
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if parameter is required when using @params
|
||||||
|
try {
|
||||||
|
final hasDefault = (param as dynamic).defaultValue != null;
|
||||||
|
if (param.isRequired && !hasDefault) {
|
||||||
|
// This might be intentional, so we don't throw an error
|
||||||
|
// but we could warn about it
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if defaultValue is not available in this analyzer version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateInjectFieldAnnotations(
|
||||||
|
FieldElement field,
|
||||||
|
List<String> annotations,
|
||||||
|
) {
|
||||||
|
if (!annotations.contains('inject')) {
|
||||||
|
return; // Not an inject field, nothing to validate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if field type is suitable for injection
|
||||||
|
final fieldType = field.type.getDisplayString();
|
||||||
|
if (fieldType == 'void') {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'Cannot inject void type',
|
||||||
|
element: field,
|
||||||
|
suggestion: 'Use a concrete type instead of void',
|
||||||
|
context: {
|
||||||
|
'field_name': field.displayName,
|
||||||
|
'field_type': fieldType,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate scope annotation if present
|
||||||
|
for (final meta in field.metadata) {
|
||||||
|
final obj = meta.computeConstantValue();
|
||||||
|
final type = obj?.type?.getDisplayString();
|
||||||
|
if (type == 'scope') {
|
||||||
|
// Empty scope name is treated as no scope (uses root scope)
|
||||||
|
// This is allowed for backward compatibility and convenience
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateModuleClassAnnotations(
|
||||||
|
ClassElement classElement,
|
||||||
|
List<String> annotations,
|
||||||
|
) {
|
||||||
|
if (!annotations.contains('module')) {
|
||||||
|
return; // Not a module class
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if class has public methods
|
||||||
|
final publicMethods = classElement.methods.where((m) => m.isPublic).toList();
|
||||||
|
if (publicMethods.isEmpty) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'Module class must have at least one public method',
|
||||||
|
element: classElement,
|
||||||
|
suggestion: 'Add public methods with @instance or @provide annotations',
|
||||||
|
context: {
|
||||||
|
'class_name': classElement.displayName,
|
||||||
|
'method_count': publicMethods.length,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that public methods have appropriate annotations
|
||||||
|
for (final method in publicMethods) {
|
||||||
|
final methodAnnotations = _getAnnotationNames(method.metadata);
|
||||||
|
if (!methodAnnotations.contains('instance') && !methodAnnotations.contains('provide')) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'Public methods in module class must have @instance or @provide annotation',
|
||||||
|
element: method,
|
||||||
|
suggestion: 'Add @instance() or @provide() annotation to the method',
|
||||||
|
context: {
|
||||||
|
'class_name': classElement.displayName,
|
||||||
|
'method_name': method.displayName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _validateInjectableClassAnnotations(
|
||||||
|
ClassElement classElement,
|
||||||
|
List<String> annotations,
|
||||||
|
) {
|
||||||
|
if (!annotations.contains('injectable')) {
|
||||||
|
return; // Not an injectable class
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if class has injectable fields
|
||||||
|
final injectFields = classElement.fields.where((f) {
|
||||||
|
final fieldAnnotations = _getAnnotationNames(f.metadata);
|
||||||
|
return fieldAnnotations.contains('inject');
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Allow injectable classes without @inject fields to generate empty mixins
|
||||||
|
// This can be useful for classes that will have @inject fields added later
|
||||||
|
// or for testing purposes
|
||||||
|
if (injectFields.isEmpty) {
|
||||||
|
// Just log a warning but don't throw an exception
|
||||||
|
// print('Warning: Injectable class ${classElement.displayName} has no @inject fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that injectable fields are properly declared
|
||||||
|
for (final field in injectFields) {
|
||||||
|
// Injectable fields should be late final for immutability after injection
|
||||||
|
if (!field.isFinal) {
|
||||||
|
throw AnnotationValidationException(
|
||||||
|
'Injectable fields should be final for immutability',
|
||||||
|
element: field,
|
||||||
|
suggestion: 'Add final keyword to injectable field (preferably late final)',
|
||||||
|
context: {
|
||||||
|
'class_name': classElement.displayName,
|
||||||
|
'field_name': field.displayName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if field is late (recommended pattern)
|
||||||
|
try {
|
||||||
|
final isLate = (field as dynamic).isLate ?? false;
|
||||||
|
if (!isLate) {
|
||||||
|
// This is a warning, not an error - late final is recommended but not required
|
||||||
|
// We could add a warning system later
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if isLate is not available in this analyzer version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,13 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
import 'package:source_gen/source_gen.dart';
|
|
||||||
|
|
||||||
import 'bind_parameters_spec.dart';
|
import 'bind_parameters_spec.dart';
|
||||||
import 'metadata_utils.dart';
|
import 'metadata_utils.dart';
|
||||||
|
import 'exceptions.dart';
|
||||||
|
import 'type_parser.dart';
|
||||||
|
import 'annotation_validator.dart';
|
||||||
|
|
||||||
enum BindingType {
|
enum BindingType {
|
||||||
instance,
|
instance,
|
||||||
@@ -105,10 +108,42 @@ class BindSpec {
|
|||||||
final indentStr = ' ' * indent;
|
final indentStr = ' ' * indent;
|
||||||
final provide = _generateProvideClause(indent);
|
final provide = _generateProvideClause(indent);
|
||||||
final postfix = _generatePostfix();
|
final postfix = _generatePostfix();
|
||||||
return '$indentStr'
|
|
||||||
'bind<$returnType>()'
|
// Create the full single-line version first
|
||||||
'$provide'
|
final singleLine = '${indentStr}bind<$returnType>()$provide$postfix;';
|
||||||
'$postfix;';
|
|
||||||
|
// Check if we need multiline formatting
|
||||||
|
final needsMultiline = singleLine.length > 80 || provide.contains('\n');
|
||||||
|
|
||||||
|
if (!needsMultiline) {
|
||||||
|
return singleLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multiline formatting, check if we need to break after bind<Type>()
|
||||||
|
if (provide.contains('\n')) {
|
||||||
|
// Provider clause is already multiline
|
||||||
|
if (postfix.isNotEmpty) {
|
||||||
|
// If there's a postfix, break after bind<Type>()
|
||||||
|
final multilinePostfix = _generateMultilinePostfix(indent);
|
||||||
|
return '${indentStr}bind<$returnType>()'
|
||||||
|
'\n${' ' * (indent + 4)}$provide'
|
||||||
|
'$multilinePostfix;';
|
||||||
|
} else {
|
||||||
|
// No postfix, keep bind<Type>() with provide start
|
||||||
|
return '${indentStr}bind<$returnType>()$provide;';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Simple multiline: break after bind<Type>()
|
||||||
|
if (postfix.isNotEmpty) {
|
||||||
|
final multilinePostfix = _generateMultilinePostfix(indent);
|
||||||
|
return '${indentStr}bind<$returnType>()'
|
||||||
|
'\n${' ' * (indent + 4)}$provide'
|
||||||
|
'$multilinePostfix;';
|
||||||
|
} else {
|
||||||
|
return '${indentStr}bind<$returnType>()'
|
||||||
|
'\n${' ' * (indent + 4)}$provide;';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal method: decides how the provide clause should be generated by param kind.
|
// Internal method: decides how the provide clause should be generated by param kind.
|
||||||
@@ -122,6 +157,7 @@ class BindSpec {
|
|||||||
// Safe variable name for parameters.
|
// Safe variable name for parameters.
|
||||||
const paramVar = 'args';
|
const paramVar = 'args';
|
||||||
final fnArgs = parameters.map((p) => p.generateArg(paramVar)).join(', ');
|
final fnArgs = parameters.map((p) => p.generateArg(paramVar)).join(', ');
|
||||||
|
// Use multiline format only if args are long or contain newlines
|
||||||
final multiLine = fnArgs.length > 60 || fnArgs.contains('\n');
|
final multiLine = fnArgs.length > 60 || fnArgs.contains('\n');
|
||||||
switch (bindingType) {
|
switch (bindingType) {
|
||||||
case BindingType.instance:
|
case BindingType.instance:
|
||||||
@@ -146,7 +182,11 @@ class BindSpec {
|
|||||||
/// EN / RU: Supports only injected dependencies, not runtime (@params).
|
/// EN / RU: Supports only injected dependencies, not runtime (@params).
|
||||||
String _generatePlainProvideClause(int indent) {
|
String _generatePlainProvideClause(int indent) {
|
||||||
final argsStr = parameters.map((p) => p.generateArg()).join(', ');
|
final argsStr = parameters.map((p) => p.generateArg()).join(', ');
|
||||||
final multiLine = argsStr.length > 60 || argsStr.contains('\n');
|
|
||||||
|
// Check if we need multiline formatting based on total line length
|
||||||
|
final singleLineCall = '$methodName($argsStr)';
|
||||||
|
final needsMultiline = singleLineCall.length >= 45 || argsStr.contains('\n');
|
||||||
|
|
||||||
switch (bindingType) {
|
switch (bindingType) {
|
||||||
case BindingType.instance:
|
case BindingType.instance:
|
||||||
return isAsyncInstance
|
return isAsyncInstance
|
||||||
@@ -154,13 +194,21 @@ class BindSpec {
|
|||||||
: '.toInstance($methodName($argsStr))';
|
: '.toInstance($methodName($argsStr))';
|
||||||
case BindingType.provide:
|
case BindingType.provide:
|
||||||
if (isAsyncProvide) {
|
if (isAsyncProvide) {
|
||||||
return multiLine
|
if (needsMultiline) {
|
||||||
? '.toProvideAsync(\n${' ' * (indent + 2)}() => $methodName($argsStr))'
|
final lambdaIndent = (isSingleton || named != null) ? indent + 6 : indent + 2;
|
||||||
: '.toProvideAsync(() => $methodName($argsStr))';
|
final closingIndent = (isSingleton || named != null) ? indent + 4 : indent;
|
||||||
|
return '.toProvideAsync(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
|
||||||
|
} else {
|
||||||
|
return '.toProvideAsync(() => $methodName($argsStr))';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return multiLine
|
if (needsMultiline) {
|
||||||
? '.toProvide(\n${' ' * (indent + 2)}() => $methodName($argsStr))'
|
final lambdaIndent = (isSingleton || named != null) ? indent + 6 : indent + 2;
|
||||||
: '.toProvide(() => $methodName($argsStr))';
|
final closingIndent = (isSingleton || named != null) ? indent + 4 : indent;
|
||||||
|
return '.toProvide(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
|
||||||
|
} else {
|
||||||
|
return '.toProvide(() => $methodName($argsStr))';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +219,20 @@ class BindSpec {
|
|||||||
final singletonPart = isSingleton ? '.singleton()' : '';
|
final singletonPart = isSingleton ? '.singleton()' : '';
|
||||||
return '$namePart$singletonPart';
|
return '$namePart$singletonPart';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// EN / RU: Generates multiline postfix with proper indentation.
|
||||||
|
String _generateMultilinePostfix(int indent) {
|
||||||
|
final parts = <String>[];
|
||||||
|
if (named != null) {
|
||||||
|
parts.add(".withName('$named')");
|
||||||
|
}
|
||||||
|
if (isSingleton) {
|
||||||
|
parts.add('.singleton()');
|
||||||
|
}
|
||||||
|
if (parts.isEmpty) return '';
|
||||||
|
|
||||||
|
return parts.map((part) => '\n${' ' * (indent + 4)}$part').join('');
|
||||||
|
}
|
||||||
|
|
||||||
/// -------------------------------------------------------------------------
|
/// -------------------------------------------------------------------------
|
||||||
/// fromMethod
|
/// fromMethod
|
||||||
@@ -186,73 +248,93 @@ class BindSpec {
|
|||||||
/// асинхронности. Если нет @instance или @provide — кидает ошибку.
|
/// асинхронности. Если нет @instance или @provide — кидает ошибку.
|
||||||
/// -------------------------------------------------------------------------
|
/// -------------------------------------------------------------------------
|
||||||
static BindSpec fromMethod(MethodElement method) {
|
static BindSpec fromMethod(MethodElement method) {
|
||||||
var returnType = method.returnType.getDisplayString();
|
try {
|
||||||
|
// Validate method annotations
|
||||||
|
AnnotationValidator.validateMethodAnnotations(method);
|
||||||
|
|
||||||
|
// Parse return type using improved type parser
|
||||||
|
final parsedReturnType = TypeParser.parseType(method.returnType, method);
|
||||||
|
|
||||||
|
final methodName = method.displayName;
|
||||||
|
|
||||||
|
// Check for @singleton annotation.
|
||||||
|
final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton');
|
||||||
|
|
||||||
final methodName = method.displayName;
|
// Get @named value if present.
|
||||||
// Check for @singleton annotation.
|
final named = MetadataUtils.getNamedValue(method.metadata);
|
||||||
final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton');
|
|
||||||
|
|
||||||
// Get @named value if present.
|
// Parse each method parameter.
|
||||||
final named = MetadataUtils.getNamedValue(method.metadata);
|
final params = <BindParameterSpec>[];
|
||||||
|
bool hasParams = false;
|
||||||
|
for (final p in method.parameters) {
|
||||||
|
final typeStr = p.type.getDisplayString();
|
||||||
|
final paramNamed = MetadataUtils.getNamedValue(p.metadata);
|
||||||
|
final isParams = MetadataUtils.anyMeta(p.metadata, 'params');
|
||||||
|
if (isParams) hasParams = true;
|
||||||
|
params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams));
|
||||||
|
}
|
||||||
|
|
||||||
// Parse each method parameter.
|
// Determine bindingType: @instance or @provide.
|
||||||
final params = <BindParameterSpec>[];
|
final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance');
|
||||||
bool hasParams = false;
|
final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide');
|
||||||
for (final p in method.parameters) {
|
|
||||||
final typeStr = p.type.getDisplayString();
|
if (!hasInstance && !hasProvide) {
|
||||||
final paramNamed = MetadataUtils.getNamedValue(p.metadata);
|
throw AnnotationValidationException(
|
||||||
final isParams = MetadataUtils.anyMeta(p.metadata, 'params');
|
'Method must be marked with either @instance() or @provide() annotation',
|
||||||
if (isParams) hasParams = true;
|
element: method,
|
||||||
params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams));
|
suggestion: 'Add @instance() for direct instances or @provide() for factory methods',
|
||||||
}
|
context: {
|
||||||
|
'method_name': methodName,
|
||||||
|
'return_type': parsedReturnType.displayString,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bindingType = hasInstance ? BindingType.instance : BindingType.provide;
|
||||||
|
|
||||||
// Determine bindingType: @instance or @provide.
|
// PROHIBIT @params with @instance bindings!
|
||||||
final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance');
|
if (bindingType == BindingType.instance && hasParams) {
|
||||||
final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide');
|
throw AnnotationValidationException(
|
||||||
if (!hasInstance && !hasProvide) {
|
'@params() (runtime arguments) cannot be used together with @instance()',
|
||||||
throw InvalidGenerationSourceError(
|
element: method,
|
||||||
'Method $methodName must be marked with @instance() or @provide().',
|
suggestion: 'Use @provide() instead if you want runtime arguments',
|
||||||
|
context: {
|
||||||
|
'method_name': methodName,
|
||||||
|
'binding_type': 'instance',
|
||||||
|
'has_params': hasParams,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set async flags based on parsed type
|
||||||
|
final isAsyncInstance = bindingType == BindingType.instance && parsedReturnType.isFuture;
|
||||||
|
final isAsyncProvide = bindingType == BindingType.provide && parsedReturnType.isFuture;
|
||||||
|
|
||||||
|
return BindSpec(
|
||||||
|
returnType: parsedReturnType.codeGenType,
|
||||||
|
methodName: methodName,
|
||||||
|
isSingleton: isSingleton,
|
||||||
|
named: named,
|
||||||
|
parameters: params,
|
||||||
|
bindingType: bindingType,
|
||||||
|
isAsyncInstance: isAsyncInstance,
|
||||||
|
isAsyncProvide: isAsyncProvide,
|
||||||
|
hasParams: hasParams,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is CherryPickGeneratorException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw CodeGenerationException(
|
||||||
|
'Failed to create BindSpec from method "${method.displayName}"',
|
||||||
element: method,
|
element: method,
|
||||||
|
suggestion: 'Check that the method has valid annotations and return type',
|
||||||
|
context: {
|
||||||
|
'method_name': method.displayName,
|
||||||
|
'return_type': method.returnType.getDisplayString(),
|
||||||
|
'error': e.toString(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final bindingType =
|
|
||||||
hasInstance ? BindingType.instance : BindingType.provide;
|
|
||||||
|
|
||||||
// PROHIBIT @params with @instance bindings!
|
|
||||||
if (bindingType == BindingType.instance && hasParams) {
|
|
||||||
throw InvalidGenerationSourceError(
|
|
||||||
'@params() (runtime arguments) cannot be used together with @instance() on method $methodName. '
|
|
||||||
'Use @provide() instead if you want runtime arguments.',
|
|
||||||
element: method,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Extract inner type for Future<T> and set async flags.
|
|
||||||
bool isAsyncInstance = false;
|
|
||||||
bool isAsyncProvide = false;
|
|
||||||
final futureInnerType = _extractFutureInnerType(returnType);
|
|
||||||
if (futureInnerType != null) {
|
|
||||||
returnType = futureInnerType;
|
|
||||||
if (bindingType == BindingType.instance) isAsyncInstance = true;
|
|
||||||
if (bindingType == BindingType.provide) isAsyncProvide = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BindSpec(
|
|
||||||
returnType: returnType,
|
|
||||||
methodName: methodName,
|
|
||||||
isSingleton: isSingleton,
|
|
||||||
named: named,
|
|
||||||
parameters: params,
|
|
||||||
bindingType: bindingType,
|
|
||||||
isAsyncInstance: isAsyncInstance,
|
|
||||||
isAsyncProvide: isAsyncProvide,
|
|
||||||
hasParams: hasParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// EN / RU: Extracts inner type from Future<T>, returns e.g. "T" or null.
|
|
||||||
static String? _extractFutureInnerType(String typeName) {
|
|
||||||
final match = RegExp(r'^Future<(.+)>$').firstMatch(typeName);
|
|
||||||
return match?.group(1)?.trim();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
cherrypick_generator/lib/src/exceptions.dart
Normal file
117
cherrypick_generator/lib/src/exceptions.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||||
|
// Licensed 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:analyzer/dart/element/element.dart';
|
||||||
|
import 'package:source_gen/source_gen.dart';
|
||||||
|
|
||||||
|
/// Enhanced exception class for CherryPick generator with detailed context information
|
||||||
|
class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||||
|
final String category;
|
||||||
|
final String? suggestion;
|
||||||
|
final Map<String, dynamic>? context;
|
||||||
|
|
||||||
|
CherryPickGeneratorException(
|
||||||
|
String message, {
|
||||||
|
required Element element,
|
||||||
|
required this.category,
|
||||||
|
this.suggestion,
|
||||||
|
this.context,
|
||||||
|
}) : super(
|
||||||
|
_formatMessage(message, category, suggestion, context, element),
|
||||||
|
element: element,
|
||||||
|
);
|
||||||
|
|
||||||
|
static String _formatMessage(
|
||||||
|
String message,
|
||||||
|
String category,
|
||||||
|
String? suggestion,
|
||||||
|
Map<String, dynamic>? context,
|
||||||
|
Element element,
|
||||||
|
) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
|
// Header with category
|
||||||
|
buffer.writeln('[$category] $message');
|
||||||
|
|
||||||
|
// Element context
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('Context:');
|
||||||
|
buffer.writeln(' Element: ${element.displayName}');
|
||||||
|
buffer.writeln(' Type: ${element.runtimeType}');
|
||||||
|
buffer.writeln(' Location: ${element.source?.fullName ?? 'unknown'}');
|
||||||
|
|
||||||
|
// Note: enclosingElement may not be available in all analyzer versions
|
||||||
|
try {
|
||||||
|
final enclosing = (element as dynamic).enclosingElement;
|
||||||
|
if (enclosing != null) {
|
||||||
|
buffer.writeln(' Enclosing: ${enclosing.displayName}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if enclosingElement is not available
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional context
|
||||||
|
if (context != null && context.isNotEmpty) {
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('Additional Context:');
|
||||||
|
context.forEach((key, value) {
|
||||||
|
buffer.writeln(' $key: $value');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestion
|
||||||
|
if (suggestion != null) {
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('💡 Suggestion: $suggestion');
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specific exception types for different error categories
|
||||||
|
class AnnotationValidationException extends CherryPickGeneratorException {
|
||||||
|
AnnotationValidationException(
|
||||||
|
super.message, {
|
||||||
|
required super.element,
|
||||||
|
super.suggestion,
|
||||||
|
super.context,
|
||||||
|
}) : super(category: 'ANNOTATION_VALIDATION');
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeParsingException extends CherryPickGeneratorException {
|
||||||
|
TypeParsingException(
|
||||||
|
super.message, {
|
||||||
|
required super.element,
|
||||||
|
super.suggestion,
|
||||||
|
super.context,
|
||||||
|
}) : super(category: 'TYPE_PARSING');
|
||||||
|
}
|
||||||
|
|
||||||
|
class CodeGenerationException extends CherryPickGeneratorException {
|
||||||
|
CodeGenerationException(
|
||||||
|
super.message, {
|
||||||
|
required super.element,
|
||||||
|
super.suggestion,
|
||||||
|
super.context,
|
||||||
|
}) : super(category: 'CODE_GENERATION');
|
||||||
|
}
|
||||||
|
|
||||||
|
class DependencyResolutionException extends CherryPickGeneratorException {
|
||||||
|
DependencyResolutionException(
|
||||||
|
super.message, {
|
||||||
|
required super.element,
|
||||||
|
super.suggestion,
|
||||||
|
super.context,
|
||||||
|
}) : super(category: 'DEPENDENCY_RESOLUTION');
|
||||||
|
}
|
||||||
@@ -49,10 +49,15 @@ class GeneratedClass {
|
|||||||
/// Список всех обнаруженных биндингов
|
/// Список всех обнаруженных биндингов
|
||||||
final List<BindSpec> binds;
|
final List<BindSpec> binds;
|
||||||
|
|
||||||
|
/// Source file name for the part directive
|
||||||
|
/// Имя исходного файла для part директивы
|
||||||
|
final String sourceFile;
|
||||||
|
|
||||||
GeneratedClass(
|
GeneratedClass(
|
||||||
this.className,
|
this.className,
|
||||||
this.generatedClassName,
|
this.generatedClassName,
|
||||||
this.binds,
|
this.binds,
|
||||||
|
this.sourceFile,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// -------------------------------------------------------------------------
|
/// -------------------------------------------------------------------------
|
||||||
@@ -72,13 +77,15 @@ class GeneratedClass {
|
|||||||
final className = element.displayName;
|
final className = element.displayName;
|
||||||
// Generated class name with '$' prefix (standard for generated Dart code).
|
// Generated class name with '$' prefix (standard for generated Dart code).
|
||||||
final generatedClassName = r'$' + className;
|
final generatedClassName = r'$' + className;
|
||||||
|
// Get source file name
|
||||||
|
final sourceFile = element.source.shortName;
|
||||||
// Collect bindings for all non-abstract methods.
|
// Collect bindings for all non-abstract methods.
|
||||||
final binds = element.methods
|
final binds = element.methods
|
||||||
.where((m) => !m.isAbstract)
|
.where((m) => !m.isAbstract)
|
||||||
.map(BindSpec.fromMethod)
|
.map(BindSpec.fromMethod)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return GeneratedClass(className, generatedClassName, binds);
|
return GeneratedClass(className, generatedClassName, binds, sourceFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// -------------------------------------------------------------------------
|
/// -------------------------------------------------------------------------
|
||||||
@@ -95,11 +102,17 @@ class GeneratedClass {
|
|||||||
/// и регистрирует все зависимости через методы bind<Type>()...
|
/// и регистрирует все зависимости через методы bind<Type>()...
|
||||||
/// -------------------------------------------------------------------------
|
/// -------------------------------------------------------------------------
|
||||||
String generate() {
|
String generate() {
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer()
|
||||||
|
..writeln('// dart format width=80')
|
||||||
buffer.writeln('final class $generatedClassName extends $className {');
|
..writeln('// GENERATED CODE - DO NOT MODIFY BY HAND')
|
||||||
buffer.writeln(' @override');
|
..writeln()
|
||||||
buffer.writeln(' void builder(Scope currentScope) {');
|
..writeln('// **************************************************************************')
|
||||||
|
..writeln('// ModuleGenerator')
|
||||||
|
..writeln('// **************************************************************************')
|
||||||
|
..writeln()
|
||||||
|
..writeln('final class $generatedClassName extends $className {')
|
||||||
|
..writeln(' @override')
|
||||||
|
..writeln(' void builder(Scope currentScope) {');
|
||||||
|
|
||||||
// For each binding, generate bind<Type>() code string.
|
// For each binding, generate bind<Type>() code string.
|
||||||
// Для каждого биндинга — генерируем строку bind<Type>()...
|
// Для каждого биндинга — генерируем строку bind<Type>()...
|
||||||
@@ -107,8 +120,9 @@ class GeneratedClass {
|
|||||||
buffer.writeln(bind.generateBind(4));
|
buffer.writeln(bind.generateBind(4));
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.writeln(' }');
|
buffer
|
||||||
buffer.writeln('}');
|
..writeln(' }')
|
||||||
|
..writeln('}');
|
||||||
|
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
216
cherrypick_generator/lib/src/type_parser.dart
Normal file
216
cherrypick_generator/lib/src/type_parser.dart
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||||
|
// Licensed 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:analyzer/dart/element/element.dart';
|
||||||
|
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||||
|
import 'package:analyzer/dart/element/type.dart';
|
||||||
|
import 'exceptions.dart';
|
||||||
|
|
||||||
|
/// Enhanced type parser that uses AST analysis instead of regular expressions
|
||||||
|
class TypeParser {
|
||||||
|
/// Parses a DartType and extracts detailed type information
|
||||||
|
static ParsedType parseType(DartType dartType, Element context) {
|
||||||
|
try {
|
||||||
|
return _parseTypeInternal(dartType, context);
|
||||||
|
} catch (e) {
|
||||||
|
throw TypeParsingException(
|
||||||
|
'Failed to parse type: ${dartType.getDisplayString()}',
|
||||||
|
element: context,
|
||||||
|
suggestion: 'Ensure the type is properly imported and accessible',
|
||||||
|
context: {
|
||||||
|
'original_type': dartType.getDisplayString(),
|
||||||
|
'error': e.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ParsedType _parseTypeInternal(DartType dartType, Element context) {
|
||||||
|
final displayString = dartType.getDisplayString();
|
||||||
|
final isNullable = dartType.nullabilitySuffix == NullabilitySuffix.question;
|
||||||
|
|
||||||
|
// Check if it's a Future type
|
||||||
|
if (dartType.isDartAsyncFuture) {
|
||||||
|
return _parseFutureType(dartType, context, isNullable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a generic type (List, Map, etc.)
|
||||||
|
if (dartType is ParameterizedType && dartType.typeArguments.isNotEmpty) {
|
||||||
|
return _parseGenericType(dartType, context, isNullable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple type
|
||||||
|
return ParsedType(
|
||||||
|
displayString: displayString,
|
||||||
|
coreType: displayString.replaceAll('?', ''),
|
||||||
|
isNullable: isNullable,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ParsedType _parseFutureType(DartType dartType, Element context, bool isNullable) {
|
||||||
|
if (dartType is! ParameterizedType || dartType.typeArguments.isEmpty) {
|
||||||
|
throw TypeParsingException(
|
||||||
|
'Future type must have a type argument',
|
||||||
|
element: context,
|
||||||
|
suggestion: 'Use Future<T> instead of raw Future',
|
||||||
|
context: {'type': dartType.getDisplayString()},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final innerType = dartType.typeArguments.first;
|
||||||
|
final innerParsed = _parseTypeInternal(innerType, context);
|
||||||
|
|
||||||
|
return ParsedType(
|
||||||
|
displayString: dartType.getDisplayString(),
|
||||||
|
coreType: innerParsed.coreType,
|
||||||
|
isNullable: isNullable || innerParsed.isNullable,
|
||||||
|
isFuture: true,
|
||||||
|
isGeneric: innerParsed.isGeneric,
|
||||||
|
typeArguments: innerParsed.typeArguments,
|
||||||
|
innerType: innerParsed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ParsedType _parseGenericType(ParameterizedType dartType, Element context, bool isNullable) {
|
||||||
|
final typeArguments = dartType.typeArguments
|
||||||
|
.map((arg) => _parseTypeInternal(arg, context))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final baseType = dartType.element?.name ?? dartType.getDisplayString();
|
||||||
|
|
||||||
|
return ParsedType(
|
||||||
|
displayString: dartType.getDisplayString(),
|
||||||
|
coreType: baseType,
|
||||||
|
isNullable: isNullable,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: true,
|
||||||
|
typeArguments: typeArguments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that a type is suitable for dependency injection
|
||||||
|
static void validateInjectableType(ParsedType parsedType, Element context) {
|
||||||
|
// Check for void type
|
||||||
|
if (parsedType.coreType == 'void') {
|
||||||
|
throw TypeParsingException(
|
||||||
|
'Cannot inject void type',
|
||||||
|
element: context,
|
||||||
|
suggestion: 'Use a concrete type instead of void',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dynamic type (warning)
|
||||||
|
if (parsedType.coreType == 'dynamic') {
|
||||||
|
// This could be a warning instead of an error
|
||||||
|
throw TypeParsingException(
|
||||||
|
'Using dynamic type reduces type safety',
|
||||||
|
element: context,
|
||||||
|
suggestion: 'Consider using a specific type instead of dynamic',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate nested types for complex generics
|
||||||
|
for (final typeArg in parsedType.typeArguments) {
|
||||||
|
validateInjectableType(typeArg, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a parsed type with detailed information
|
||||||
|
class ParsedType {
|
||||||
|
/// The full display string of the type (e.g., "Future<List<String>?>")
|
||||||
|
final String displayString;
|
||||||
|
|
||||||
|
/// The core type name without nullability and Future wrapper (e.g., "List<String>")
|
||||||
|
final String coreType;
|
||||||
|
|
||||||
|
/// Whether the type is nullable
|
||||||
|
final bool isNullable;
|
||||||
|
|
||||||
|
/// Whether the type is wrapped in Future
|
||||||
|
final bool isFuture;
|
||||||
|
|
||||||
|
/// Whether the type has generic parameters
|
||||||
|
final bool isGeneric;
|
||||||
|
|
||||||
|
/// Parsed type arguments for generic types
|
||||||
|
final List<ParsedType> typeArguments;
|
||||||
|
|
||||||
|
/// For Future types, the inner type
|
||||||
|
final ParsedType? innerType;
|
||||||
|
|
||||||
|
const ParsedType({
|
||||||
|
required this.displayString,
|
||||||
|
required this.coreType,
|
||||||
|
required this.isNullable,
|
||||||
|
required this.isFuture,
|
||||||
|
required this.isGeneric,
|
||||||
|
required this.typeArguments,
|
||||||
|
this.innerType,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Returns the type string suitable for code generation
|
||||||
|
String get codeGenType {
|
||||||
|
if (isFuture && innerType != null) {
|
||||||
|
return innerType!.codeGenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For generic types, include type arguments
|
||||||
|
if (isGeneric && typeArguments.isNotEmpty) {
|
||||||
|
final args = typeArguments.map((arg) => arg.codeGenType).join(', ');
|
||||||
|
return '$coreType<$args>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return coreType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this type should use tryResolve instead of resolve
|
||||||
|
bool get shouldUseTryResolve => isNullable;
|
||||||
|
|
||||||
|
/// Returns the appropriate resolve method name
|
||||||
|
String get resolveMethodName {
|
||||||
|
if (isFuture) {
|
||||||
|
return shouldUseTryResolve ? 'tryResolveAsync' : 'resolveAsync';
|
||||||
|
}
|
||||||
|
return shouldUseTryResolve ? 'tryResolve' : 'resolve';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ParsedType(displayString: $displayString, coreType: $coreType, '
|
||||||
|
'isNullable: $isNullable, isFuture: $isFuture, isGeneric: $isGeneric)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is ParsedType &&
|
||||||
|
other.displayString == displayString &&
|
||||||
|
other.coreType == coreType &&
|
||||||
|
other.isNullable == isNullable &&
|
||||||
|
other.isFuture == isFuture &&
|
||||||
|
other.isGeneric == isGeneric;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return displayString.hashCode ^
|
||||||
|
coreType.hashCode ^
|
||||||
|
isNullable.hashCode ^
|
||||||
|
isFuture.hashCode ^
|
||||||
|
isGeneric.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
415
cherrypick_generator/test/annotation_validator_test.dart
Normal file
415
cherrypick_generator/test/annotation_validator_test.dart
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
|
import 'package:analyzer/dart/constant/value.dart';
|
||||||
|
import 'package:analyzer/dart/element/type.dart';
|
||||||
|
import 'package:analyzer/source/source.dart';
|
||||||
|
import 'package:cherrypick_generator/src/annotation_validator.dart';
|
||||||
|
import 'package:cherrypick_generator/src/exceptions.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AnnotationValidator', () {
|
||||||
|
group('validateMethodAnnotations', () {
|
||||||
|
test('should pass for valid @instance method', () {
|
||||||
|
final method = _createMockMethod(
|
||||||
|
name: 'createService',
|
||||||
|
annotations: ['instance'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateMethodAnnotations(method),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for valid @provide method', () {
|
||||||
|
final method = _createMockMethod(
|
||||||
|
name: 'createService',
|
||||||
|
annotations: ['provide'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateMethodAnnotations(method),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for method with both @instance and @provide', () {
|
||||||
|
final method = _createMockMethod(
|
||||||
|
name: 'createService',
|
||||||
|
annotations: ['instance', 'provide'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateMethodAnnotations(method),
|
||||||
|
throwsA(isA<AnnotationValidationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for method with @params but no @provide', () {
|
||||||
|
final method = _createMockMethod(
|
||||||
|
name: 'createService',
|
||||||
|
annotations: ['instance', 'params'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateMethodAnnotations(method),
|
||||||
|
throwsA(isA<AnnotationValidationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for method with neither @instance nor @provide', () {
|
||||||
|
final method = _createMockMethod(
|
||||||
|
name: 'createService',
|
||||||
|
annotations: ['singleton'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateMethodAnnotations(method),
|
||||||
|
throwsA(isA<AnnotationValidationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for @provide method with @params', () {
|
||||||
|
final method = _createMockMethod(
|
||||||
|
name: 'createService',
|
||||||
|
annotations: ['provide', 'params'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateMethodAnnotations(method),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for @singleton method', () {
|
||||||
|
final method = _createMockMethod(
|
||||||
|
name: 'createService',
|
||||||
|
annotations: ['provide', 'singleton'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateMethodAnnotations(method),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('validateFieldAnnotations', () {
|
||||||
|
test('should pass for valid @inject field', () {
|
||||||
|
final field = _createMockField(
|
||||||
|
name: 'service',
|
||||||
|
annotations: ['inject'],
|
||||||
|
type: 'String',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateFieldAnnotations(field),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for @inject field with void type', () {
|
||||||
|
final field = _createMockField(
|
||||||
|
name: 'service',
|
||||||
|
annotations: ['inject'],
|
||||||
|
type: 'void',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateFieldAnnotations(field),
|
||||||
|
throwsA(isA<AnnotationValidationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for non-inject field', () {
|
||||||
|
final field = _createMockField(
|
||||||
|
name: 'service',
|
||||||
|
annotations: [],
|
||||||
|
type: 'String',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateFieldAnnotations(field),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('validateClassAnnotations', () {
|
||||||
|
test('should pass for valid @module class', () {
|
||||||
|
final classElement = _createMockClass(
|
||||||
|
name: 'AppModule',
|
||||||
|
annotations: ['module'],
|
||||||
|
methods: [
|
||||||
|
_createMockMethod(name: 'createService', annotations: ['provide']),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateClassAnnotations(classElement),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for @module class with no public methods', () {
|
||||||
|
final classElement = _createMockClass(
|
||||||
|
name: 'AppModule',
|
||||||
|
annotations: ['module'],
|
||||||
|
methods: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateClassAnnotations(classElement),
|
||||||
|
throwsA(isA<AnnotationValidationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for @module class with unannotated public methods', () {
|
||||||
|
final classElement = _createMockClass(
|
||||||
|
name: 'AppModule',
|
||||||
|
annotations: ['module'],
|
||||||
|
methods: [
|
||||||
|
_createMockMethod(name: 'createService', annotations: []),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateClassAnnotations(classElement),
|
||||||
|
throwsA(isA<AnnotationValidationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for valid @injectable class', () {
|
||||||
|
final classElement = _createMockClass(
|
||||||
|
name: 'AppService',
|
||||||
|
annotations: ['injectable'],
|
||||||
|
fields: [
|
||||||
|
_createMockField(name: 'dependency', annotations: ['inject'], type: 'String', isFinal: true),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateClassAnnotations(classElement),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for @injectable class with no inject fields', () {
|
||||||
|
final classElement = _createMockClass(
|
||||||
|
name: 'AppService',
|
||||||
|
annotations: ['injectable'],
|
||||||
|
fields: [
|
||||||
|
_createMockField(name: 'dependency', annotations: [], type: 'String'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateClassAnnotations(classElement),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for @injectable class with non-final inject fields', () {
|
||||||
|
final classElement = _createMockClass(
|
||||||
|
name: 'AppService',
|
||||||
|
annotations: ['injectable'],
|
||||||
|
fields: [
|
||||||
|
_createMockField(
|
||||||
|
name: 'dependency',
|
||||||
|
annotations: ['inject'],
|
||||||
|
type: 'String',
|
||||||
|
isFinal: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateClassAnnotations(classElement),
|
||||||
|
throwsA(isA<AnnotationValidationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for @injectable class with final inject fields', () {
|
||||||
|
final classElement = _createMockClass(
|
||||||
|
name: 'AppService',
|
||||||
|
annotations: ['injectable'],
|
||||||
|
fields: [
|
||||||
|
_createMockField(
|
||||||
|
name: 'dependency',
|
||||||
|
annotations: ['inject'],
|
||||||
|
type: 'String',
|
||||||
|
isFinal: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => AnnotationValidator.validateClassAnnotations(classElement),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock implementations for testing
|
||||||
|
MethodElement _createMockMethod({
|
||||||
|
required String name,
|
||||||
|
required List<String> annotations,
|
||||||
|
}) {
|
||||||
|
return _MockMethodElement(name, annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldElement _createMockField({
|
||||||
|
required String name,
|
||||||
|
required List<String> annotations,
|
||||||
|
required String type,
|
||||||
|
bool isFinal = false,
|
||||||
|
}) {
|
||||||
|
return _MockFieldElement(name, annotations, type, isFinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassElement _createMockClass({
|
||||||
|
required String name,
|
||||||
|
required List<String> annotations,
|
||||||
|
List<MethodElement> methods = const [],
|
||||||
|
List<FieldElement> fields = const [],
|
||||||
|
}) {
|
||||||
|
return _MockClassElement(name, annotations, methods, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockMethodElement implements MethodElement {
|
||||||
|
final String _name;
|
||||||
|
final List<String> _annotations;
|
||||||
|
|
||||||
|
_MockMethodElement(this._name, this._annotations);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Source get source => _MockSource();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get displayName => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ElementAnnotation> get metadata => _annotations.map((a) => _MockElementAnnotation(a)).toList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isPublic => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ParameterElement> get parameters => [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
DartType get returnType => _MockDartType('String');
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockFieldElement implements FieldElement {
|
||||||
|
final String _name;
|
||||||
|
final List<String> _annotations;
|
||||||
|
final String _type;
|
||||||
|
final bool _isFinal;
|
||||||
|
|
||||||
|
_MockFieldElement(this._name, this._annotations, this._type, this._isFinal);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Source get source => _MockSource();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get displayName => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ElementAnnotation> get metadata => _annotations.map((a) => _MockElementAnnotation(a)).toList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isFinal => _isFinal;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DartType get type => _MockDartType(_type);
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockClassElement implements ClassElement {
|
||||||
|
final String _name;
|
||||||
|
final List<String> _annotations;
|
||||||
|
final List<MethodElement> _methods;
|
||||||
|
final List<FieldElement> _fields;
|
||||||
|
|
||||||
|
_MockClassElement(this._name, this._annotations, this._methods, this._fields);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Source get source => _MockSource();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get displayName => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ElementAnnotation> get metadata => _annotations.map((a) => _MockElementAnnotation(a)).toList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<MethodElement> get methods => _methods;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<FieldElement> get fields => _fields;
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockElementAnnotation implements ElementAnnotation {
|
||||||
|
final String _type;
|
||||||
|
|
||||||
|
_MockElementAnnotation(this._type);
|
||||||
|
|
||||||
|
@override
|
||||||
|
DartObject? computeConstantValue() {
|
||||||
|
return _MockDartObject(_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockDartObject implements DartObject {
|
||||||
|
final String _type;
|
||||||
|
|
||||||
|
_MockDartObject(this._type);
|
||||||
|
|
||||||
|
@override
|
||||||
|
DartType? get type => _MockDartType(_type);
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockDartType implements DartType {
|
||||||
|
final String _name;
|
||||||
|
|
||||||
|
_MockDartType(this._name);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getDisplayString({bool withNullability = true}) => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
class _MockSource implements Source {
|
||||||
|
@override
|
||||||
|
String get fullName => 'mock_source.dart';
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
@@ -245,7 +245,10 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
result,
|
result,
|
||||||
equals(
|
equals(
|
||||||
" bind<ApiClient>().toProvideAsync(() => createApiClient()).withName('mainApi').singleton();"));
|
" bind<ApiClient>()\n"
|
||||||
|
" .toProvideAsync(() => createApiClient())\n"
|
||||||
|
" .withName('mainApi')\n"
|
||||||
|
" .singleton();"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle different indentation', () {
|
test('should handle different indentation', () {
|
||||||
|
|||||||
231
cherrypick_generator/test/type_parser_test.dart
Normal file
231
cherrypick_generator/test/type_parser_test.dart
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
|
import 'package:analyzer/source/source.dart';
|
||||||
|
import 'package:cherrypick_generator/src/type_parser.dart';
|
||||||
|
import 'package:cherrypick_generator/src/exceptions.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('TypeParser', () {
|
||||||
|
group('parseType', () {
|
||||||
|
test('should parse simple types correctly', () {
|
||||||
|
// This would require setting up analyzer infrastructure
|
||||||
|
// For now, we'll test the ParsedType class directly
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse Future types correctly', () {
|
||||||
|
// This would require setting up analyzer infrastructure
|
||||||
|
// For now, we'll test the ParsedType class directly
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse nullable types correctly', () {
|
||||||
|
// This would require setting up analyzer infrastructure
|
||||||
|
// For now, we'll test the ParsedType class directly
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TypeParsingException for invalid types', () {
|
||||||
|
// This would require setting up analyzer infrastructure
|
||||||
|
// For now, we'll test the ParsedType class directly
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('validateInjectableType', () {
|
||||||
|
test('should throw for void type', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'void',
|
||||||
|
coreType: 'void',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => TypeParser.validateInjectableType(parsedType, _createMockElement()),
|
||||||
|
throwsA(isA<TypeParsingException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw for dynamic type', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'dynamic',
|
||||||
|
coreType: 'dynamic',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => TypeParser.validateInjectableType(parsedType, _createMockElement()),
|
||||||
|
throwsA(isA<TypeParsingException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass for valid types', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'String',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => TypeParser.validateInjectableType(parsedType, _createMockElement()),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ParsedType', () {
|
||||||
|
test('should return correct codeGenType for simple types', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'String',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedType.codeGenType, equals('String'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct codeGenType for Future types', () {
|
||||||
|
final innerType = ParsedType(
|
||||||
|
displayString: 'String',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'Future<String>',
|
||||||
|
coreType: 'Future<String>',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: true,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
innerType: innerType,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedType.codeGenType, equals('String'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct resolveMethodName for sync types', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'String',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedType.resolveMethodName, equals('resolve'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct resolveMethodName for nullable sync types', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'String?',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: true,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedType.resolveMethodName, equals('tryResolve'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct resolveMethodName for async types', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'Future<String>',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: true,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedType.resolveMethodName, equals('resolveAsync'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct resolveMethodName for nullable async types', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'Future<String?>',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: true,
|
||||||
|
isFuture: true,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedType.resolveMethodName, equals('tryResolveAsync'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should implement equality correctly', () {
|
||||||
|
final parsedType1 = ParsedType(
|
||||||
|
displayString: 'String',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final parsedType2 = ParsedType(
|
||||||
|
displayString: 'String',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedType1, equals(parsedType2));
|
||||||
|
expect(parsedType1.hashCode, equals(parsedType2.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should implement toString correctly', () {
|
||||||
|
final parsedType = ParsedType(
|
||||||
|
displayString: 'String',
|
||||||
|
coreType: 'String',
|
||||||
|
isNullable: false,
|
||||||
|
isFuture: false,
|
||||||
|
isGeneric: false,
|
||||||
|
typeArguments: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = parsedType.toString();
|
||||||
|
expect(result, contains('ParsedType'));
|
||||||
|
expect(result, contains('String'));
|
||||||
|
expect(result, contains('isNullable: false'));
|
||||||
|
expect(result, contains('isFuture: false'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock element for testing
|
||||||
|
Element _createMockElement() {
|
||||||
|
return _MockElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockElement implements Element {
|
||||||
|
@override
|
||||||
|
String get displayName => 'MockElement';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'MockElement';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Source? get source => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user