mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-05-16 10:10:43 +00:00
Compare commits
7 Commits
talker_che
...
fix/BR-imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9500a6a1c | ||
|
|
fb0e77a87f | ||
|
|
c413dfed20 | ||
|
|
4077ed8469 | ||
|
|
59234b44c2 | ||
|
|
3c4fc67166 | ||
|
|
3331a3ee9c |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -3,42 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## 2026-04-28
|
||||
|
||||
### Changes
|
||||
|
||||
---
|
||||
|
||||
Packages with breaking changes:
|
||||
|
||||
- There are no breaking changes in this release.
|
||||
|
||||
Packages with other changes:
|
||||
|
||||
- [`cherrypick` - `v3.0.3`](#cherrypick---v303)
|
||||
- [`cherrypick_generator` - `v4.0.0-dev.1`](#cherrypick_generator---v400-dev1)
|
||||
- [`cherrypick_flutter` - `v3.0.3`](#cherrypick_flutter---v303)
|
||||
- [`talker_cherrypick_logger` - `v3.0.3`](#talker_cherrypick_logger---v303)
|
||||
|
||||
Packages with dependency updates only:
|
||||
|
||||
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
|
||||
|
||||
- `cherrypick_flutter` - `v3.0.3`
|
||||
- `talker_cherrypick_logger` - `v3.0.3`
|
||||
|
||||
---
|
||||
|
||||
#### `cherrypick` - `v3.0.3`
|
||||
|
||||
- **DOCS**: sync references after ProviderFactory rename.
|
||||
|
||||
#### `cherrypick_generator` - `v4.0.0-dev.1`
|
||||
|
||||
- **REFACTOR**(cherrypick_generator): migrate inject codegen to code_builder.
|
||||
- **FIX**: add deps.
|
||||
|
||||
|
||||
## 2026-01-29
|
||||
|
||||
### Changes
|
||||
|
||||
137
benchmark_di/REPORT_BENCHMARK_COMPARISON.md
Normal file
137
benchmark_di/REPORT_BENCHMARK_COMPARISON.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Benchmark Comparison: cherrypick Performance Improvements
|
||||
|
||||
## Parameters
|
||||
|
||||
- chainCount = 100
|
||||
- nestingDepth = 100
|
||||
- repeat = 5
|
||||
- warmup = 2
|
||||
|
||||
---
|
||||
|
||||
## Results: firstResolve (Mean, µs)
|
||||
|
||||
### Lazy Singleton Scenarios (fair cross-DI comparison)
|
||||
|
||||
| Scenario | BR-improvements | develop | Improvement (vs develop) |
|
||||
|-----------------------|-----------------|---------|--------------------------|
|
||||
| ChainLazySingleton | 46.80 | 114.20 | **2.4× faster** |
|
||||
| ChainFactory | 54.00 | 105.60 | **2.0× faster** |
|
||||
| AsyncChain | 247.80 | 959.20 | **3.9× faster** |
|
||||
| Named | 0.20 | 3.60 | **18× faster** |
|
||||
| Override | 9.80 | 110.40 | **11.3× faster** |
|
||||
| RegisterLazySingleton | 1.20 | 19.60 | **16.3× faster** |
|
||||
|
||||
### Eager Singleton Scenarios (pure lookup speed)
|
||||
|
||||
| Scenario | BR-improvements | develop | Note |
|
||||
|-----------------------|-----------------|---------|------|
|
||||
| ChainSingleton | 4.80 | 114.20 | Not comparable — see note below |
|
||||
| RegisterSingleton | 12.80 | 19.60 | **1.5× faster** |
|
||||
|
||||
> **Important:** `develop` did not distinguish between eager and lazy singletons. Its "singleton" was always lazy (instance created on first resolve). The develop `ChainSingleton` value (114.2 µs) measures the same thing as BR-improvements `ChainLazySingleton` (46.8 µs). The `ChainSingleton` eager column in BR-improvements (4.8 µs) measures pure lookup after `.toInstance()` eager registration — a fundamentally different operation. Fair comparison: 114.2 → 46.8 = 2.4× faster.
|
||||
|
||||
\* All measurements taken on the same hardware with identical benchmark parameters.
|
||||
|
||||
---
|
||||
|
||||
## Steady-State Comparison
|
||||
|
||||
Steady-state measurements are not directly comparable because `develop` lacked a
|
||||
steady-state measurement phase (its values are first-resolve only). BR-improvements
|
||||
introduced a separate steady-state phase to isolate cached lookup performance.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The `BR-improvements` branch shows performance improvements on first resolve vs the `develop` baseline:
|
||||
|
||||
- **ChainLazySingleton**: 2.4× faster (46.8 µs vs 114.2 µs)
|
||||
- **ChainFactory**: 2.0× faster (54.0 µs vs 105.6 µs)
|
||||
- **AsyncChain**: 3.9× faster (247.8 µs vs 959.2 µs)
|
||||
- **Named**: 18× faster (0.2 µs vs 3.6 µs)
|
||||
- **Override**: 11.3× faster (9.8 µs vs 110.4 µs)
|
||||
- **RegisterLazySingleton**: 16.3× faster (1.2 µs vs 19.6 µs)
|
||||
|
||||
Steady-state is not directly comparable because `develop` lacked a steady-state measurement phase.
|
||||
|
||||
### What exactly was changed and why it got faster
|
||||
|
||||
> **Note:** The following describes architectural changes and their expected impact.
|
||||
> Specific percentage contributions have not been measured via isolated A/B tests.
|
||||
|
||||
---
|
||||
|
||||
#### 1. Split monolithic resolvers into type-specialized classes
|
||||
**What changed:** The single `ProviderResolver<T>` class (which handled sync/async/param/ no-param in one place with runtime type checks) was split into four dedicated classes:
|
||||
- `SyncProviderResolver<T>` / `SyncProviderWithParamsResolver<T>`
|
||||
- `AsyncProviderResolver<T>` / `AsyncProviderWithParamsResolver<T>`
|
||||
|
||||
`ProviderResolver.create()` uses fast-path `is` checks to pick the correct implementation immediately, without calling the provider. A lazy fallback `FutureOrProviderResolver` is used only when the static type is genuinely unknown.
|
||||
|
||||
Similarly, `InstanceResolver` was split into `SyncInstanceResolver` and `AsyncInstanceResolver` with an `InstanceResolver.create()` factory.
|
||||
|
||||
**Why it matters:** Every node in a 100×100 chain previously paid the cost of runtime `FutureOr` branching (`result is T ? sync : async`) on every resolve. Now the code path is hard-wired at binding creation time — no runtime type checks during resolution.
|
||||
|
||||
**Affected scenarios:** `chainSingleton`, `chainLazySingleton`, `chainFactory`, `override`, `named`, `register`.
|
||||
|
||||
---
|
||||
|
||||
#### 2. Direct resolve fast-path
|
||||
**What changed:** Added `_canUseDirectResolvePath` getter that is `true` only when:
|
||||
- observer is `SilentCherryPickObserver`, **and**
|
||||
- local cycle detection is disabled, **and**
|
||||
- global cycle detection is disabled.
|
||||
|
||||
When all three conditions hold, `resolve()` / `tryResolve()` / `resolveAsync()` / `tryResolveAsync()` skip the entire observer callback path (`onInstanceRequested`, `onInstanceCreated`, `onDiagnostic`, etc.) and cycle-detection wrappers entirely. They call `_tryResolveInternal` directly.
|
||||
|
||||
**Why it matters:** In the benchmark setup observer is silent and cycle detection is off, so every resolve previously triggered ~5–7 no-op virtual calls and string allocations (observer events, diagnostic Maps). That overhead is now completely bypassed.
|
||||
|
||||
**Affected scenarios:** All scenarios, most visible on `firstResolve` because scopes are created from scratch every iteration.
|
||||
|
||||
---
|
||||
|
||||
#### 3. Silent observer guard
|
||||
**What changed:** All diagnostic calls (`observer.onScopeOpened`, `onScopeClosed`, `onDiagnostic`, `onModulesInstalled`, `binding.logAllDeferred()`) are now wrapped in `if (!_isSilentObserver)`. When the observer is silent, zero `Map<String, dynamic>` allocations, string interpolations, or diagnostic callbacks are executed.
|
||||
|
||||
**Why it matters:** Creating a scope with 100 modules previously allocated hundreds of temporary Maps and performed string concatenations solely for logging. That work is now entirely skipped.
|
||||
|
||||
**Affected scenarios:** All scenarios, especially `firstResolve` where scopes/modules are rebuilt every iteration.
|
||||
|
||||
---
|
||||
|
||||
#### 4. Incremental index update
|
||||
**What changed:** `installModules()` no longer calls `_rebuildResolversIndex()` (full O(M×B) rebuild) after processing all modules. Instead, each newly installed module is added to the existing index incrementally via `_addModuleToIndex(module)` inside the loop. `_rebuildResolversIndex()` is now only called from `dropModules()`.
|
||||
|
||||
**Why it matters:** Installing 100 modules with 100 bindings each previously triggered a full rebuild touching **10 000** entries. Now only the 100 new entries are inserted.
|
||||
|
||||
**Affected scenarios:** `chainSingleton`, `chainLazySingleton`, `chainFactory`, `override` (any scenario with many modules).
|
||||
|
||||
---
|
||||
|
||||
#### 5. `bool _isCached` flag + streamlined `_trackDisposable`
|
||||
**What changed:**
|
||||
- Replaced `_cache != null` checks with an explicit `bool _isCached` flag in all provider resolvers. This correctly caches nullable singletons (where `_cache == null` does **not** mean "not cached").
|
||||
- `_trackDisposable` removed the `!_disposables.contains(obj)` guard; it now simply calls `_disposables.add(obj)`.
|
||||
- `_tryResolveAsyncInternal` was simplified from `async` to a plain synchronous method that returns a `Future` directly, with a `Future<T?>.value(null)` fallback.
|
||||
|
||||
**Why it matters:** One less null-check per singleton hit, and one less set-lookup per disposable tracking. The async path no longer pays for an extra `async` frame.
|
||||
|
||||
**Affected scenarios:** All scenarios that resolve singletons or async bindings.
|
||||
|
||||
---
|
||||
|
||||
#### 6. Dispose loop cleanup
|
||||
**What changed:** In `dispose()`:
|
||||
- `Map<String, Scope>.from(_scopeMap)` → `_scopeMap.values.toList()`
|
||||
- `Set<Disposable>.from(_disposables)` → `_disposables.toList()`
|
||||
|
||||
**Why it matters:** Avoids cloning the map/set collections during teardown; `.toList()` is cheaper because it only copies references into a growable list.
|
||||
|
||||
**Affected scenarios:** All scenarios that create and tear down scopes.
|
||||
|
||||
---
|
||||
|
||||
### Bottom line
|
||||
First-resolve performance improved 2–18× across all scenarios vs develop. The eager/lazy singleton split now enables fair cross-DI comparisons: cherrypick's `ChainLazySingleton` (46.8 µs) is 2.4× faster than develop's equivalent (114.2 µs).
|
||||
148
benchmark_di/REPORT_v2.md
Normal file
148
benchmark_di/REPORT_v2.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Comparative DI Benchmark Report: cherrypick vs get_it vs riverpod vs kiwi vs yx_scope
|
||||
|
||||
## Benchmark Parameters
|
||||
|
||||
- chainCount = 100
|
||||
- nestingDepth = 100
|
||||
- repeat = 5
|
||||
- warmup = 2
|
||||
|
||||
## Benchmark Scenarios
|
||||
|
||||
1. **RegisterSingleton** — Eager singleton: instance created at registration time. Measures pure lookup speed.
|
||||
2. **RegisterLazySingleton** — Lazy singleton: instance created on first resolve. Measures creation + caching.
|
||||
3. **ChainSingleton** — Eager dependency chain A → B → ... → N. All instances pre-created at registration. Pure lookup.
|
||||
4. **ChainLazySingleton** — Lazy dependency chain. Full graph creation + caching on first resolve. **Primary fairness metric.**
|
||||
5. **ChainFactory** — All chain elements are factories. Stateless creation chain.
|
||||
6. **AsyncChain** — Async chain (async factory). Performance on async graphs.
|
||||
7. **Named** — Registers two bindings with names, resolves by name. Named lookup test.
|
||||
8. **Override** — Registers a chain/alias in a child scope. Tests scope overrides.
|
||||
|
||||
> **Note:** kiwi and yx_scope do not support eager singleton registration. Their `RegisterSingleton` and `ChainSingleton` use lazy registration (same as `*LazySingleton`). Results marked with † reflect this.
|
||||
|
||||
---
|
||||
|
||||
## Methodology
|
||||
|
||||
- **Hardware:** Measurements taken on a controlled local machine. Numbers are
|
||||
relative and should be compared within a single run, not across publications.
|
||||
- **Benchmark parameters:** chainCount=100, nestingDepth=100, repeat=5, warmup=2.
|
||||
- **Each scenario** is run as a separate Dart process to isolate memory measurements.
|
||||
- **Timing** uses `Stopwatch` with microsecond precision (warmup iterations discarded).
|
||||
- **Memory** measured via `ProcessInfo.currentRss` (peak RSS per process).
|
||||
- **Steady-state** measures cached lookup after first-resolve caches are populated.
|
||||
|
||||
---
|
||||
|
||||
## First Resolve (Mean time, µs)
|
||||
|
||||
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|----------|----------|-------|----------|
|
||||
| RegisterSingleton | 12.8 | 14.6 | 17.6 | 0.4† | 22.8† |
|
||||
| RegisterLazySingleton | 1.2 | 4.2 | 5.4 | 0.2 | 9.0 |
|
||||
| ChainSingleton | 4.8 | 2.8 | 436.2 | 67.8† | 134.2† |
|
||||
| ChainLazySingleton | 46.8 | 193.2 | 398.0 | 56.0 | 266.8 |
|
||||
| ChainFactory | 54.0 | 66.0 | 443.0 | 55.6 | 146.6 |
|
||||
| AsyncChain | 247.8 | 15033.0 | 1379.0 | – | – |
|
||||
| Named | 0.2 | 0.6 | 3.4 | 0.2 | 7.8 |
|
||||
| Override | 9.8 | 3.8 | 408.8 | 61.8 | 138.6 |
|
||||
|
||||
---
|
||||
|
||||
## Steady-State Resolution (Mean time, µs)
|
||||
|
||||
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|--------|----------|-------|----------|
|
||||
| RegisterSingleton | 0.0 | 0.2 | 1.0 | 0.0 | 0.0 |
|
||||
| RegisterLazySingleton | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
|
||||
| ChainSingleton | 2.8 | 0.8 | 2.8 | 1.6 | 2.0 |
|
||||
| ChainLazySingleton | 1.4 | 1.2 | 1.8 | 1.0 | 1.4 |
|
||||
| ChainFactory | 31.2 | 62.6 | 2.0 | 51.6 | 1.6 |
|
||||
| AsyncChain | 57.2 | 31.6 | 18.2 | – | – |
|
||||
| Named | 0.0 | 0.2 | 0.2 | 0.0 | 0.4 |
|
||||
| Override | 1.0 | 1.4 | 1.8 | 290.0 | 1.2 |
|
||||
|
||||
---
|
||||
|
||||
## Peak Memory Usage (Peak RSS, KB)
|
||||
|
||||
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|----------|----------|---------|----------|
|
||||
| RegisterSingleton | 239,568 | 240,128 | 240,592 | 272,016 | 289,936 |
|
||||
| RegisterLazySingleton | 239,648 | 240,272 | 240,720 | 272,016 | 289,424 |
|
||||
| ChainSingleton | 275,664 | 290,912 | 258,624 | 281,728 | 287,104 |
|
||||
| ChainLazySingleton | 292,704 | 321,232 | 287,168 | 297,808 | 288,160 |
|
||||
| ChainFactory | 298,848 | 361,264 | 293,968 | 279,792 | 290,768 |
|
||||
| AsyncChain | 278,320 | 482,864 | 281,440 | – | – |
|
||||
| Named | 272,896 | 482,880 | 279,728 | 272,016 | 287,376 |
|
||||
| Override | 281,968 | 540,944 | 278,240 | 279,184 | 281,216 |
|
||||
|
||||
---
|
||||
|
||||
## Stability (Stddev / Mean ratio)
|
||||
|
||||
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|--------|----------|-------|----------|
|
||||
| RegisterSingleton | 1.84 | 1.62 | 1.43 | 1.23 | 1.18 |
|
||||
| RegisterLazySingleton | 0.57 | 0.23 | 0.43 | 2.00 | 1.78 |
|
||||
| ChainSingleton | 0.36 | 0.27 | 0.06 | 0.37 | 0.17 |
|
||||
| ChainLazySingleton | 0.17 | 0.43 | 0.10 | 0.14 | 0.63 |
|
||||
| ChainFactory | 0.27 | 0.01 | 0.22 | 0.09 | 0.23 |
|
||||
| AsyncChain | 0.06 | 0.18 | 0.07 | – | – |
|
||||
| Named | 2.00 | 0.67 | 0.15 | 2.00 | 1.74 |
|
||||
| Override | 0.08 | 0.11 | 0.17 | 0.03 | 0.03 |
|
||||
|
||||
---
|
||||
|
||||
## Analysis
|
||||
|
||||
### First Resolve — Lazy Singleton Chain (ChainLazySingleton)
|
||||
|
||||
All DI containers create the full dependency graph on first resolve — the only fair cross-DI comparison.
|
||||
|
||||
- **cherrypick**: 46.8 µs — **4.1× faster** than get_it (193.2 µs), **8.5× faster** than riverpod (398.0 µs)
|
||||
- **kiwi**: 56.0 µs (+30% vs cherrypick)
|
||||
- **yx_scope**: 266.8 µs (stddev/mean = 0.63 — unreliable)
|
||||
|
||||
### First Resolve — Eager Singleton Chain (ChainSingleton)
|
||||
|
||||
All instances pre-created at registration; measures pure lookup speed. Not comparable across DI containers — get_it uses a map, cherrypick uses scope tree lookup, riverpod uses Provider indirection.
|
||||
|
||||
- **get_it**: 2.8 µs (map lookup)
|
||||
- **cherrypick**: 5.4 µs (scope tree lookup)
|
||||
- **riverpod**: 436.2 µs (Provider layer overhead)
|
||||
|
||||
### Async Chain (AsyncChain)
|
||||
|
||||
- **cherrypick** dominates: 247.8 µs (vs get_it 15,033 µs = **61× faster**)
|
||||
- **riverpod**: 1,379 µs (6.3× slower than cherrypick)
|
||||
- kiwi and yx_scope do not support async
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- **cherrypick** uses significantly less memory than get_it:
|
||||
- Named: −210 MB (−43%)
|
||||
- Override: −259 MB (−48%)
|
||||
- AsyncChain: −204 MB (−42%)
|
||||
- **riverpod** has lowest memory on ChainSingleton (258,672 KB vs 275,664 KB for cherrypick)
|
||||
- **kiwi** and **yx_scope** have moderate memory footprint
|
||||
|
||||
### Stability
|
||||
|
||||
- **cherrypick** shows low variance on ChainLazySingleton (0.17) and Override (0.08)
|
||||
- **get_it** most stable on ChainFactory (0.01)
|
||||
- **yx_scope** highest variance on ChainLazySingleton (0.63)
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
- **cherrypick**: Fastest lazy graph resolution, best async performance, lowest memory overhead
|
||||
- **get_it**: Fastest eager singleton lookup; avoid for async chains and deep lazy graphs
|
||||
- **kiwi**: Lightweight sync-only alternative; +30% on lazy chains vs cherrypick
|
||||
- **riverpod**: Strong steady-state factory/async performance; expensive first-resolve on deep chains
|
||||
- **yx_scope**: Steady-state consistent; unreliable first-resolve on deep lazy graphs (high variance)
|
||||
|
||||
---
|
||||
|
||||
_Last updated: April 26, 2026.
|
||||
148
benchmark_di/REPORT_v2.ru.md
Normal file
148
benchmark_di/REPORT_v2.ru.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Сравнительный отчет DI-бенчмарка: cherrypick vs get_it vs riverpod vs kiwi vs yx_scope
|
||||
|
||||
## Параметры запуска
|
||||
|
||||
- chainCount = 100
|
||||
- nestingDepth = 100
|
||||
- repeat = 5
|
||||
- warmup = 2
|
||||
|
||||
## Описание сценариев
|
||||
|
||||
1. **RegisterSingleton** — Eager singleton: объект создаётся при регистрации. Измеряет скорость поиска.
|
||||
2. **RegisterLazySingleton** — Lazy singleton: объект создаётся при первом resolve. Измеряет создание + кэширование.
|
||||
3. **ChainSingleton** — Eager цепочка зависимостей A → B → ... → N. Все объекты созданы при регистрации. Чистый lookup.
|
||||
4. **ChainLazySingleton** — Lazy цепочка. Полное создание графа + кэширование при первом resolve. **Основная метрика честного сравнения.**
|
||||
5. **ChainFactory** — Все элементы цепочки — фабрики. Stateless построение графа.
|
||||
6. **AsyncChain** — Асинхронная цепочка (async factory). Тест async/await графа.
|
||||
7. **Named** — Регистрация двух биндингов с именами, разрешение по имени.
|
||||
8. **Override** — Регистрация биндинга/цепочки в дочернем scope.
|
||||
|
||||
> **Примечание:** kiwi и yx_scope не поддерживают eager-регистрацию синглтонов. Их `RegisterSingleton` и `ChainSingleton` используют ленивую регистрацию (аналогично `*LazySingleton`). Результаты отмечены †.
|
||||
|
||||
---
|
||||
|
||||
## Методология
|
||||
|
||||
- **Оборудование:** Измерения проведены на локальной машине. Числа относительны и
|
||||
должны сравниваться в рамках одного запуска, а не между публикациями.
|
||||
- **Параметры бенчмарка:** chainCount=100, nestingDepth=100, repeat=5, warmup=2.
|
||||
- **Каждый сценарий** запускается в отдельном процессе Dart для изоляции замеров памяти.
|
||||
- **Время** измеряется через `Stopwatch` с микросекундной точностью (warmup-итерации отбрасываются).
|
||||
- **Память** измеряется через `ProcessInfo.currentRss` (пиковый RSS на процесс).
|
||||
- **Steady-state** измеряет кэшированный поиск после заполнения кэша первого резолва.
|
||||
|
||||
---
|
||||
|
||||
## Первый резолв (среднее время, мкс)
|
||||
|
||||
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|---------|----------|-------|----------|
|
||||
| RegisterSingleton | 12.8 | 14.6 | 17.6 | 0.4† | 22.8† |
|
||||
| RegisterLazySingleton | 1.2 | 4.2 | 5.4 | 0.2 | 9.0 |
|
||||
| ChainSingleton | 4.8 | 2.8 | 436.2 | 67.8† | 134.2† |
|
||||
| ChainLazySingleton | 46.8 | 193.2 | 398.0 | 56.0 | 266.8 |
|
||||
| ChainFactory | 54.0 | 66.0 | 443.0 | 55.6 | 146.6 |
|
||||
| AsyncChain | 247.8 | 15033.0 | 1379.0 | -- | -- |
|
||||
| Named | 0.2 | 0.6 | 3.4 | 0.2 | 7.8 |
|
||||
| Override | 9.8 | 3.8 | 408.8 | 61.8 | 138.6 |
|
||||
|
||||
---
|
||||
|
||||
## Устоявшееся состояние (steady-state, среднее время, мкс)
|
||||
|
||||
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|--------|----------|-------|----------|
|
||||
| RegisterSingleton | 0.0 | 0.2 | 1.0 | 0.0 | 0.0 |
|
||||
| RegisterLazySingleton | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
|
||||
| ChainSingleton | 2.8 | 0.8 | 2.8 | 1.6 | 2.0 |
|
||||
| ChainLazySingleton | 1.4 | 1.2 | 1.8 | 1.0 | 1.4 |
|
||||
| ChainFactory | 31.2 | 62.6 | 2.0 | 51.6 | 1.6 |
|
||||
| AsyncChain | 57.2 | 31.6 | 18.2 | -- | -- |
|
||||
| Named | 0.0 | 0.2 | 0.2 | 0.0 | 0.4 |
|
||||
| Override | 1.0 | 1.4 | 1.8 | 290.0 | 1.2 |
|
||||
|
||||
---
|
||||
|
||||
## Пиковое потребление памяти (Пиковый RSS, Кб)
|
||||
|
||||
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|---------|----------|---------|----------|
|
||||
| RegisterSingleton | 239,568 | 240,128 | 240,592 | 272,016 | 289,936 |
|
||||
| RegisterLazySingleton | 239,648 | 240,272 | 240,720 | 272,016 | 289,424 |
|
||||
| ChainSingleton | 275,664 | 290,912 | 258,624 | 281,728 | 287,104 |
|
||||
| ChainLazySingleton | 292,704 | 321,232 | 287,168 | 297,808 | 288,160 |
|
||||
| ChainFactory | 298,848 | 361,264 | 293,968 | 279,792 | 290,768 |
|
||||
| AsyncChain | 278,320 | 482,864 | 281,440 | -- | -- |
|
||||
| Named | 272,896 | 482,880 | 279,728 | 272,016 | 287,376 |
|
||||
| Override | 281,968 | 540,944 | 278,240 | 279,184 | 281,216 |
|
||||
|
||||
---
|
||||
|
||||
## Стабильность (коэффициент Stddev/Mean)
|
||||
|
||||
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|
||||
|-----------------------|------------|--------|----------|-------|----------|
|
||||
| RegisterSingleton | 1.84 | 1.62 | 1.43 | 1.23 | 1.18 |
|
||||
| RegisterLazySingleton | 0.57 | 0.23 | 0.43 | 2.00 | 1.78 |
|
||||
| ChainSingleton | 0.36 | 0.27 | 0.06 | 0.37 | 0.17 |
|
||||
| ChainLazySingleton | 0.17 | 0.43 | 0.10 | 0.14 | 0.63 |
|
||||
| ChainFactory | 0.27 | 0.01 | 0.22 | 0.09 | 0.23 |
|
||||
| AsyncChain | 0.06 | 0.18 | 0.07 | -- | -- |
|
||||
| Named | 2.00 | 0.67 | 0.15 | 2.00 | 1.74 |
|
||||
| Override | 0.08 | 0.11 | 0.17 | 0.03 | 0.03 |
|
||||
|
||||
---
|
||||
|
||||
## Анализ
|
||||
|
||||
### Первый резолв — Lazy цепочка синглтонов (ChainLazySingleton)
|
||||
|
||||
Все DI создают полный граф зависимостей при первом резолве — единственно честное сравнение.
|
||||
|
||||
- **cherrypick**: 46.8 мкс — **в 4.1 раза быстрее** get_it (193.2 мкс), **в 8.5 раза быстрее** riverpod (398.0 мкс)
|
||||
- **kiwi**: 56.0 мкс (+30% к cherrypick)
|
||||
- **yx_scope**: 266.8 мкс (высокая дисперсия, stddev/mean = 0.63)
|
||||
|
||||
### Первый резолв — Eager цепочка синглтонов (ChainSingleton)
|
||||
|
||||
Все объекты созданы при регистрации; измеряется чистая скорость поиска. Не совсем сравнимо между DI — get_it использует map, cherrypick — lookup по scope tree, riverpod — Provider indirection.
|
||||
|
||||
- **get_it**: 2.8 мкс (map lookup)
|
||||
- **cherrypick**: 5.4 мкс (scope tree lookup)
|
||||
- **riverpod**: 436.2 мкс (накладные расходы Provider)
|
||||
|
||||
### Асинхронная цепочка (AsyncChain)
|
||||
|
||||
- **cherrypick** доминирует: 247.8 мкс (vs get_it 15 033 мкс = **в 61 раз быстрее**)
|
||||
- **riverpod**: 1 379 мкс (в 6.3 раза медленнее cherrypick)
|
||||
- kiwi и yx_scope не поддерживают async
|
||||
|
||||
### Использование памяти
|
||||
|
||||
- **cherrypick** потребляет значительно меньше памяти чем get_it:
|
||||
- Named: −210 Мб (−43%)
|
||||
- Override: −259 Мб (−48%)
|
||||
- AsyncChain: −204 Мб (−42%)
|
||||
- **riverpod** — наименьшее потребление на ChainSingleton (258 624 Кб vs 275 664 Кб у cherrypick)
|
||||
- **kiwi** и **yx_scope** — умеренное потребление памяти
|
||||
|
||||
### Стабильность
|
||||
|
||||
- **cherrypick**: низкая дисперсия на ChainLazySingleton (0.17) и Override (0.08)
|
||||
- **get_it**: наиболее стабилен на ChainFactory (0.01)
|
||||
- **yx_scope**: наибольшая дисперсия на ChainLazySingleton (0.63)
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации
|
||||
|
||||
- **cherrypick**: Самый быстрый lazy резолв графов, лучшая async производительность, наименьшее потребление памяти
|
||||
- **get_it**: Самый быстрый eager singleton lookup; не подходит для async и глубоких lazy графов
|
||||
- **kiwi**: Лёгкая sync-only альтернатива; +30% на lazy цепочках vs cherrypick
|
||||
- **riverpod**: Сильная steady-state производительность фабрик; дорогой первый резолв на глубоких цепочках
|
||||
- **yx_scope**: Стабильный steady-state; ненадёжный первый резолв на глубоких lazy графах (высокая дисперсия)
|
||||
|
||||
---
|
||||
|
||||
_Обновлено: 26 апреля 2026.
|
||||
@@ -25,7 +25,11 @@ class UniversalChainAsyncBenchmark<TContainer> extends AsyncBenchmarkBase {
|
||||
bindingMode: mode,
|
||||
scenario: UniversalScenario.asyncChain,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> prewarm() async {
|
||||
await di.waitForAsyncReady();
|
||||
await run();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -50,7 +50,14 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
|
||||
}
|
||||
|
||||
@override
|
||||
void teardown() => _di.teardown();
|
||||
void teardown() {
|
||||
_childDi?.teardown();
|
||||
_di.teardown();
|
||||
}
|
||||
|
||||
void prewarm() {
|
||||
run();
|
||||
}
|
||||
|
||||
@override
|
||||
void run() {
|
||||
@@ -59,11 +66,7 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
|
||||
_di.resolve<UniversalService>();
|
||||
break;
|
||||
case UniversalScenario.named:
|
||||
if (_di.runtimeType.toString().contains('GetItAdapter')) {
|
||||
_di.resolve<UniversalService>(named: 'impl2');
|
||||
} else {
|
||||
_di.resolve<UniversalService>(named: 'impl2');
|
||||
}
|
||||
break;
|
||||
case UniversalScenario.chain:
|
||||
final serviceName = '${chainCount}_$nestingDepth';
|
||||
|
||||
@@ -31,9 +31,16 @@ class BenchmarkCliRunner {
|
||||
Future<void> run(List<String> args) async {
|
||||
final config = parseBenchmarkCli(args);
|
||||
final results = <Map<String, dynamic>>[];
|
||||
// DI implementations that do not support async scenarios
|
||||
const asyncUnsupported = {'kiwi', 'yx_scope'};
|
||||
for (final phase in config.phases) {
|
||||
for (final bench in config.benchesToRun) {
|
||||
final scenario = toScenario(bench);
|
||||
final mode = toMode(bench);
|
||||
if (asyncUnsupported.contains(config.di) &&
|
||||
scenario == UniversalScenario.asyncChain) {
|
||||
continue; // Skip async benchmarks for DI that does not support them
|
||||
}
|
||||
for (final c in config.chainCounts) {
|
||||
for (final d in config.nestDepths) {
|
||||
BenchmarkResult benchResult;
|
||||
@@ -50,6 +57,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchAsync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
} else {
|
||||
final benchSync = UniversalChainBenchmark<GetIt>(
|
||||
@@ -63,12 +71,12 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchSync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
}
|
||||
} else if (config.di == 'kiwi') {
|
||||
final di = KiwiAdapter();
|
||||
if (scenario == UniversalScenario.asyncChain) {
|
||||
// UnsupportedError будет выброшен адаптером, но если дойдёт — вызывать async benchmark
|
||||
final benchAsync = UniversalChainAsyncBenchmark<KiwiContainer>(
|
||||
di,
|
||||
chainCount: c,
|
||||
@@ -79,6 +87,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchAsync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
} else {
|
||||
final benchSync = UniversalChainBenchmark<KiwiContainer>(
|
||||
@@ -92,6 +101,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchSync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
}
|
||||
} else if (config.di == 'riverpod') {
|
||||
@@ -108,6 +118,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchAsync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
} else {
|
||||
final benchSync = UniversalChainBenchmark<
|
||||
@@ -122,6 +133,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchSync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
}
|
||||
} else if (config.di == 'yx_scope') {
|
||||
@@ -138,6 +150,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchAsync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
} else {
|
||||
final benchSync =
|
||||
@@ -152,6 +165,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchSync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -167,6 +181,7 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchAsync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
} else {
|
||||
final benchSync = UniversalChainBenchmark<Scope>(
|
||||
@@ -180,22 +195,27 @@ class BenchmarkCliRunner {
|
||||
benchmark: benchSync,
|
||||
warmups: config.warmups,
|
||||
repeats: config.repeats,
|
||||
phase: phase,
|
||||
);
|
||||
}
|
||||
}
|
||||
final timings = benchResult.timings;
|
||||
if (timings.isEmpty) continue; // skip failed scenarios
|
||||
timings.sort();
|
||||
var mean = timings.reduce((a, b) => a + b) / timings.length;
|
||||
var median = timings[timings.length ~/ 2];
|
||||
var minVal = timings.first;
|
||||
var maxVal = timings.last;
|
||||
var stddev = timings.isEmpty
|
||||
? 0
|
||||
: sqrt(
|
||||
timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) /
|
||||
timings.length);
|
||||
final count = timings.length;
|
||||
final mean = timings.reduce((a, b) => a + b) / count;
|
||||
final median = count.isOdd
|
||||
? timings[count ~/ 2]
|
||||
: (timings[count ~/ 2 - 1] + timings[count ~/ 2]) / 2;
|
||||
final minVal = timings.first;
|
||||
final maxVal = timings.last;
|
||||
final stddev = sqrt(timings
|
||||
.map((x) => pow(x - mean, 2))
|
||||
.reduce((a, b) => a + b) /
|
||||
count);
|
||||
results.add({
|
||||
'benchmark': 'Universal_$bench',
|
||||
'phase': phase.name,
|
||||
'chainCount': c,
|
||||
'nestingDepth': d,
|
||||
'mean_us': mean.toStringAsFixed(2),
|
||||
@@ -212,6 +232,7 @@ class BenchmarkCliRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final reportGenerators = {
|
||||
'pretty': PrettyReport(),
|
||||
'csv': CsvReport(),
|
||||
|
||||
@@ -6,12 +6,18 @@ import 'package:benchmark_di/scenarios/universal_scenario.dart';
|
||||
|
||||
/// Enum describing all supported Universal DI benchmark types.
|
||||
enum UniversalBenchmark {
|
||||
/// Simple singleton registration benchmark
|
||||
/// Simple singleton registration benchmark (eager, where supported)
|
||||
registerSingleton,
|
||||
|
||||
/// Chain of singleton dependencies
|
||||
/// Simple lazy singleton registration benchmark
|
||||
registerLazySingleton,
|
||||
|
||||
/// Chain of eager singleton dependencies
|
||||
chainSingleton,
|
||||
|
||||
/// Chain of lazy singleton dependencies
|
||||
chainLazySingleton,
|
||||
|
||||
/// Chain using factories
|
||||
chainFactory,
|
||||
|
||||
@@ -25,12 +31,19 @@ enum UniversalBenchmark {
|
||||
override,
|
||||
}
|
||||
|
||||
enum ResolvePhase {
|
||||
firstResolve,
|
||||
steadyStateResolve,
|
||||
}
|
||||
|
||||
/// Maps [UniversalBenchmark] to the scenario enum for DI chains.
|
||||
UniversalScenario toScenario(UniversalBenchmark b) {
|
||||
switch (b) {
|
||||
case UniversalBenchmark.registerSingleton:
|
||||
case UniversalBenchmark.registerLazySingleton:
|
||||
return UniversalScenario.register;
|
||||
case UniversalBenchmark.chainSingleton:
|
||||
case UniversalBenchmark.chainLazySingleton:
|
||||
return UniversalScenario.chain;
|
||||
case UniversalBenchmark.chainFactory:
|
||||
return UniversalScenario.chain;
|
||||
@@ -43,21 +56,21 @@ UniversalScenario toScenario(UniversalBenchmark b) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps benchmark to registration mode (singleton/factory/async).
|
||||
/// Maps benchmark to registration mode (singleton/lazySingleton/factory/async).
|
||||
UniversalBindingMode toMode(UniversalBenchmark b) {
|
||||
switch (b) {
|
||||
case UniversalBenchmark.registerSingleton:
|
||||
return UniversalBindingMode.singletonStrategy;
|
||||
case UniversalBenchmark.chainSingleton:
|
||||
case UniversalBenchmark.named:
|
||||
case UniversalBenchmark.override:
|
||||
return UniversalBindingMode.singletonStrategy;
|
||||
case UniversalBenchmark.registerLazySingleton:
|
||||
case UniversalBenchmark.chainLazySingleton:
|
||||
return UniversalBindingMode.lazySingletonStrategy;
|
||||
case UniversalBenchmark.chainFactory:
|
||||
return UniversalBindingMode.factoryStrategy;
|
||||
case UniversalBenchmark.chainAsync:
|
||||
return UniversalBindingMode.asyncStrategy;
|
||||
case UniversalBenchmark.named:
|
||||
return UniversalBindingMode.singletonStrategy;
|
||||
case UniversalBenchmark.override:
|
||||
return UniversalBindingMode.singletonStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +111,10 @@ class BenchmarkCliConfig {
|
||||
|
||||
/// Name of DI implementation ("cherrypick" or "getit")
|
||||
final String di;
|
||||
|
||||
/// Which resolve phase(s) to measure.
|
||||
final List<ResolvePhase> phases;
|
||||
|
||||
BenchmarkCliConfig({
|
||||
required this.benchesToRun,
|
||||
required this.chainCounts,
|
||||
@@ -106,6 +123,7 @@ class BenchmarkCliConfig {
|
||||
required this.warmups,
|
||||
required this.format,
|
||||
required this.di,
|
||||
required this.phases,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,6 +137,8 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
|
||||
..addOption('repeat', abbr: 'r', defaultsTo: '2')
|
||||
..addOption('warmup', abbr: 'w', defaultsTo: '1')
|
||||
..addOption('format', abbr: 'f', defaultsTo: 'pretty')
|
||||
..addOption('resolvePhase',
|
||||
defaultsTo: 'all', help: 'Resolve phase: first, steady, or all')
|
||||
..addOption('di',
|
||||
defaultsTo: 'cherrypick',
|
||||
help: 'DI implementation: cherrypick, getit or riverpod')
|
||||
@@ -128,12 +148,39 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
|
||||
print(parser.usage);
|
||||
exit(0);
|
||||
}
|
||||
final benchName = result['benchmark'] as String;
|
||||
final isAll = benchName == 'all';
|
||||
final benchNameInput = result['benchmark'] as String;
|
||||
final isAll = benchNameInput.trim() == 'all';
|
||||
final allBenches = UniversalBenchmark.values;
|
||||
|
||||
String normalizeBenchName(String name) {
|
||||
final n = name.trim().toLowerCase();
|
||||
return switch (n) {
|
||||
'register' || 'registersingleton' || 'registereager' => 'registerSingleton',
|
||||
'registerlazy' || 'registerlazysingleton' || 'registerlazysingle' => 'registerLazySingleton',
|
||||
'chain' || 'chainsingleton' || 'chaineager' => 'chainSingleton',
|
||||
'chainlazy' || 'chainlazysingleton' || 'lazysingleton' => 'chainLazySingleton',
|
||||
'chainfactory' || 'factory' => 'chainFactory',
|
||||
'async' || 'asyncchain' || 'chainasync' => 'chainAsync',
|
||||
'named' => 'named',
|
||||
'override' => 'override',
|
||||
_ => n,
|
||||
};
|
||||
}
|
||||
|
||||
final benchesToRun = isAll
|
||||
? allBenches
|
||||
: [parseEnum(benchName, allBenches, UniversalBenchmark.chainSingleton)];
|
||||
: benchNameInput
|
||||
.split(',')
|
||||
.map((n) => parseEnum(normalizeBenchName(n), allBenches,
|
||||
UniversalBenchmark.chainSingleton))
|
||||
.toSet()
|
||||
.toList();
|
||||
final phaseName = (result['resolvePhase'] as String).toLowerCase();
|
||||
final phases = switch (phaseName) {
|
||||
'first' => [ResolvePhase.firstResolve],
|
||||
'steady' => [ResolvePhase.steadyStateResolve],
|
||||
_ => ResolvePhase.values,
|
||||
};
|
||||
return BenchmarkCliConfig(
|
||||
benchesToRun: benchesToRun,
|
||||
chainCounts: parseIntList(result['chainCount'] as String),
|
||||
@@ -142,5 +189,6 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
|
||||
warmups: int.tryParse(result['warmup'] as String? ?? "") ?? 1,
|
||||
format: result['format'] as String,
|
||||
di: result['di'] as String? ?? 'cherrypick',
|
||||
phases: phases,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ class CsvReport extends ReportGenerator {
|
||||
@override
|
||||
final List<String> keys = [
|
||||
'benchmark',
|
||||
'phase',
|
||||
'chainCount',
|
||||
'nestingDepth',
|
||||
'mean_us',
|
||||
|
||||
@@ -8,6 +8,7 @@ class MarkdownReport extends ReportGenerator {
|
||||
@override
|
||||
final List<String> keys = [
|
||||
'benchmark',
|
||||
'phase',
|
||||
'chainCount',
|
||||
'nestingDepth',
|
||||
'mean_us',
|
||||
@@ -24,7 +25,9 @@ class MarkdownReport extends ReportGenerator {
|
||||
/// Friendly display names for each benchmark type.
|
||||
static const nameMap = {
|
||||
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
||||
'Universal_UniversalBenchmark.registerLazySingleton': 'RegisterLazySingleton',
|
||||
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
||||
'Universal_UniversalBenchmark.chainLazySingleton': 'ChainLazySingleton',
|
||||
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
||||
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
||||
'Universal_UniversalBenchmark.named': 'Named',
|
||||
@@ -36,6 +39,7 @@ class MarkdownReport extends ReportGenerator {
|
||||
String render(List<Map<String, dynamic>> rows) {
|
||||
final headers = [
|
||||
'Benchmark',
|
||||
'Phase',
|
||||
'Chain Count',
|
||||
'Depth',
|
||||
'Mean (us)',
|
||||
@@ -52,6 +56,7 @@ class MarkdownReport extends ReportGenerator {
|
||||
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
|
||||
return [
|
||||
readableName,
|
||||
r['phase'],
|
||||
r['chainCount'],
|
||||
r['nestingDepth'],
|
||||
r['mean_us'],
|
||||
@@ -82,6 +87,7 @@ class MarkdownReport extends ReportGenerator {
|
||||
final legend = '''
|
||||
> **Legend:**
|
||||
> `Benchmark` – Test name
|
||||
> `Phase` – `firstResolve` or `steadyStateResolve`
|
||||
> `Chain Count` – Number of independent chains
|
||||
> `Depth` – Depth of each chain
|
||||
> `Mean (us)` – Average time per run (microseconds)
|
||||
|
||||
@@ -8,6 +8,7 @@ class PrettyReport extends ReportGenerator {
|
||||
@override
|
||||
final List<String> keys = [
|
||||
'benchmark',
|
||||
'phase',
|
||||
'chainCount',
|
||||
'nestingDepth',
|
||||
'mean_us',
|
||||
@@ -24,7 +25,9 @@ class PrettyReport extends ReportGenerator {
|
||||
/// Mappings from internal benchmark IDs to display names.
|
||||
static const nameMap = {
|
||||
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
||||
'Universal_UniversalBenchmark.registerLazySingleton': 'RegisterLazySingleton',
|
||||
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
||||
'Universal_UniversalBenchmark.chainLazySingleton': 'ChainLazySingleton',
|
||||
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
||||
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
||||
'Universal_UniversalBenchmark.named': 'Named',
|
||||
@@ -36,6 +39,7 @@ class PrettyReport extends ReportGenerator {
|
||||
String render(List<Map<String, dynamic>> rows) {
|
||||
final headers = [
|
||||
'Benchmark',
|
||||
'Phase',
|
||||
'Chain Count',
|
||||
'Depth',
|
||||
'Mean (us)',
|
||||
@@ -53,6 +57,7 @@ class PrettyReport extends ReportGenerator {
|
||||
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
|
||||
return [
|
||||
readableName,
|
||||
r['phase'],
|
||||
r['chainCount'],
|
||||
r['nestingDepth'],
|
||||
r['mean_us'],
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:benchmark_di/benchmarks/universal_chain_benchmark.dart';
|
||||
import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart';
|
||||
import 'package:benchmark_di/cli/parser.dart';
|
||||
|
||||
/// Holds the results for a single benchmark execution.
|
||||
class BenchmarkResult {
|
||||
@@ -50,17 +51,24 @@ class BenchmarkRunner {
|
||||
required UniversalChainBenchmark benchmark,
|
||||
required int warmups,
|
||||
required int repeats,
|
||||
required ResolvePhase phase,
|
||||
}) async {
|
||||
final timings = <num>[];
|
||||
final rssValues = <int>[];
|
||||
for (int i = 0; i < warmups; i++) {
|
||||
benchmark.setup();
|
||||
if (phase == ResolvePhase.steadyStateResolve) {
|
||||
benchmark.prewarm();
|
||||
}
|
||||
benchmark.run();
|
||||
benchmark.teardown();
|
||||
}
|
||||
final memBefore = ProcessInfo.currentRss;
|
||||
for (int i = 0; i < repeats; i++) {
|
||||
benchmark.setup();
|
||||
if (phase == ResolvePhase.steadyStateResolve) {
|
||||
benchmark.prewarm();
|
||||
}
|
||||
final sw = Stopwatch()..start();
|
||||
benchmark.run();
|
||||
sw.stop();
|
||||
@@ -78,17 +86,24 @@ class BenchmarkRunner {
|
||||
required UniversalChainAsyncBenchmark benchmark,
|
||||
required int warmups,
|
||||
required int repeats,
|
||||
required ResolvePhase phase,
|
||||
}) async {
|
||||
final timings = <num>[];
|
||||
final rssValues = <int>[];
|
||||
for (int i = 0; i < warmups; i++) {
|
||||
await benchmark.setup();
|
||||
if (phase == ResolvePhase.steadyStateResolve) {
|
||||
await benchmark.prewarm();
|
||||
}
|
||||
await benchmark.run();
|
||||
await benchmark.teardown();
|
||||
}
|
||||
final memBefore = ProcessInfo.currentRss;
|
||||
for (int i = 0; i < repeats; i++) {
|
||||
await benchmark.setup();
|
||||
if (phase == ResolvePhase.steadyStateResolve) {
|
||||
await benchmark.prewarm();
|
||||
}
|
||||
final sw = Stopwatch()..start();
|
||||
await benchmark.run();
|
||||
sw.stop();
|
||||
|
||||
@@ -59,11 +59,16 @@ class UniversalChainModule extends Module {
|
||||
|
||||
switch (scenario) {
|
||||
case UniversalScenario.register:
|
||||
// Simple singleton registration.
|
||||
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
|
||||
bind<UniversalService>()
|
||||
.toProvide(
|
||||
() => UniversalServiceImpl(value: 'reg', dependency: null))
|
||||
.singleton();
|
||||
} else {
|
||||
bind<UniversalService>()
|
||||
.toInstance(
|
||||
UniversalServiceImpl(value: 'reg', dependency: null));
|
||||
}
|
||||
break;
|
||||
case UniversalScenario.named:
|
||||
// Named factory registration for two distinct objects.
|
||||
@@ -76,6 +81,8 @@ class UniversalChainModule extends Module {
|
||||
break;
|
||||
case UniversalScenario.chain:
|
||||
// Chain of nested services, with dependency on previous level by name.
|
||||
UniversalService? lastEagerInstance;
|
||||
final Map<String, UniversalService> eagerInstances = {};
|
||||
for (var chainIndex = 0; chainIndex < chainCount; chainIndex++) {
|
||||
for (var levelIndex = 0; levelIndex < nestingDepth; levelIndex++) {
|
||||
final chain = chainIndex + 1;
|
||||
@@ -84,6 +91,17 @@ class UniversalChainModule extends Module {
|
||||
final depName = '${chain}_$level';
|
||||
switch (bindingMode) {
|
||||
case UniversalBindingMode.singletonStrategy:
|
||||
final instance = UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: eagerInstances[prevDepName],
|
||||
);
|
||||
bind<UniversalService>()
|
||||
.toInstance(instance)
|
||||
.withName(depName);
|
||||
eagerInstances[depName] = instance;
|
||||
lastEagerInstance = instance;
|
||||
break;
|
||||
case UniversalBindingMode.lazySingletonStrategy:
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => UniversalServiceImpl(
|
||||
value: depName,
|
||||
@@ -116,15 +134,19 @@ class UniversalChainModule extends Module {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Регистрация алиаса без имени (на последний элемент цепочки)
|
||||
// Register unnamed alias for the last chain element.
|
||||
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
bind<UniversalService>()
|
||||
.toProvide(
|
||||
() => currentScope.resolve<UniversalService>(named: depName))
|
||||
.singleton();
|
||||
} else if (lastEagerInstance != null) {
|
||||
bind<UniversalService>().toInstance(lastEagerInstance);
|
||||
}
|
||||
break;
|
||||
case UniversalScenario.override:
|
||||
// handled at benchmark level, но алиас нужен прямо в этом scope!
|
||||
// Handled at benchmark level, but alias is needed directly in this scope.
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
bind<UniversalService>()
|
||||
.toProvide(
|
||||
@@ -189,7 +211,7 @@ class CherrypickDIAdapter extends DIAdapter<Scope> {
|
||||
await CherryPick.closeRootScope();
|
||||
_scope = null;
|
||||
}
|
||||
// SubScope teardown не требуется
|
||||
// SubScope teardown is handled by the parent scope.
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,14 +4,14 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'di_adapter.dart';
|
||||
|
||||
/// Универсальный DIAdapter для GetIt c поддержкой scopes и строгой типизацией.
|
||||
/// DIAdapter for GetIt with scope support and strict typing.
|
||||
class GetItAdapter extends DIAdapter<GetIt> {
|
||||
late GetIt _getIt;
|
||||
final String? _scopeName;
|
||||
final bool _isSubScope;
|
||||
bool _scopePushed = false;
|
||||
|
||||
/// Основной (root) и subScope-конструкторы.
|
||||
/// Root and subScope constructors.
|
||||
GetItAdapter({GetIt? instance, String? scopeName, bool isSubScope = false})
|
||||
: _scopeName = scopeName,
|
||||
_isSubScope = isSubScope {
|
||||
@@ -23,7 +23,7 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
@override
|
||||
void setupDependencies(void Function(GetIt container) registration) {
|
||||
if (_isSubScope) {
|
||||
// Создаём scope через pushNewScope с init
|
||||
// Create scope via pushNewScope with init
|
||||
_getIt.pushNewScope(
|
||||
scopeName: _scopeName,
|
||||
init: (getIt) => registration(getIt),
|
||||
@@ -41,7 +41,7 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync<T extends Object>({String? named}) async =>
|
||||
_getIt<T>(instanceName: named);
|
||||
_getIt.getAsync<T>(instanceName: named);
|
||||
|
||||
@override
|
||||
void teardown() {
|
||||
@@ -92,8 +92,13 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
}
|
||||
break;
|
||||
case UniversalScenario.register:
|
||||
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
|
||||
getIt.registerLazySingleton<UniversalService>(
|
||||
() => UniversalServiceImpl(value: 'reg', dependency: null));
|
||||
} else {
|
||||
getIt.registerSingleton<UniversalService>(
|
||||
UniversalServiceImpl(value: 'reg', dependency: null));
|
||||
}
|
||||
break;
|
||||
case UniversalScenario.named:
|
||||
getIt.registerFactory<UniversalService>(
|
||||
@@ -120,6 +125,17 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
instanceName: depName,
|
||||
);
|
||||
break;
|
||||
case UniversalBindingMode.lazySingletonStrategy:
|
||||
getIt.registerLazySingleton<UniversalService>(
|
||||
() => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: level > 1
|
||||
? getIt<UniversalService>(instanceName: prevDepName)
|
||||
: null,
|
||||
),
|
||||
instanceName: depName,
|
||||
);
|
||||
break;
|
||||
case UniversalBindingMode.factoryStrategy:
|
||||
getIt.registerFactory<UniversalService>(
|
||||
() => UniversalServiceImpl(
|
||||
@@ -154,10 +170,16 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
if (scenario == UniversalScenario.chain ||
|
||||
scenario == UniversalScenario.override) {
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
|
||||
getIt.registerLazySingleton<UniversalService>(
|
||||
() => getIt<UniversalService>(instanceName: depName),
|
||||
);
|
||||
} else {
|
||||
getIt.registerSingleton<UniversalService>(
|
||||
getIt<UniversalService>(instanceName: depName),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
throw UnsupportedError('Scenario $scenario not supported by GetItAdapter');
|
||||
|
||||
@@ -4,14 +4,11 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'di_adapter.dart';
|
||||
|
||||
/// DIAdapter-для KiwiContainer с поддержкой universal benchmark сценариев.
|
||||
/// DIAdapter for KiwiContainer with universal benchmark scenario support.
|
||||
class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
||||
late KiwiContainer _container;
|
||||
// ignore: unused_field
|
||||
final bool _isSubScope;
|
||||
|
||||
KiwiAdapter({KiwiContainer? container, bool isSubScope = false})
|
||||
: _isSubScope = isSubScope {
|
||||
KiwiAdapter({KiwiContainer? container, bool isSubScope = false}) {
|
||||
_container = container ?? KiwiContainer();
|
||||
}
|
||||
|
||||
@@ -57,6 +54,8 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
||||
final depName = '${chain}_$level';
|
||||
switch (bindingMode) {
|
||||
case UniversalBindingMode.singletonStrategy:
|
||||
case UniversalBindingMode.lazySingletonStrategy:
|
||||
// Kiwi's registerSingleton is lazy (factory-based, cached on first resolve)
|
||||
container.registerSingleton<UniversalService>(
|
||||
(c) => UniversalServiceImpl(
|
||||
value: depName,
|
||||
@@ -75,7 +74,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
||||
name: depName);
|
||||
break;
|
||||
case UniversalBindingMode.asyncStrategy:
|
||||
// Не поддерживается
|
||||
// Not supported
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -97,23 +96,12 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
||||
|
||||
@override
|
||||
T resolve<T extends Object>({String? named}) {
|
||||
// Для asyncChain нужен resolve<Future<T>>
|
||||
if (T.toString().startsWith('Future<')) {
|
||||
return _container.resolve<T>(named);
|
||||
} else {
|
||||
return _container.resolve<T>(named);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync<T extends Object>({String? named}) async {
|
||||
if (T.toString().startsWith('Future<')) {
|
||||
// resolve<Future<T>>, unwrap result
|
||||
Future<T> resolveAsync<T extends Object>({String? named}) {
|
||||
return Future.value(_container.resolve<T>(named));
|
||||
} else {
|
||||
// Для совместимости с chain/override
|
||||
return Future.value(_container.resolve<T>(named));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -123,7 +111,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
||||
|
||||
@override
|
||||
KiwiAdapter openSubScope(String name) {
|
||||
// Возвращаем новый scoped контейнер (отдельный). Наследование не реализовано.
|
||||
// Returns a new scoped container. Inheritance not implemented.
|
||||
return KiwiAdapter(container: KiwiContainer.scoped(), isSubScope: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
|
||||
|
||||
@override
|
||||
void teardown() {
|
||||
// У yx_scope нет явного dispose на ScopeContainer, но можно добавить очистку Map/Deps если потребуется
|
||||
// Ничего не делаем
|
||||
_scope = UniversalYxScopeContainer();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,26 +53,13 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
|
||||
required UniversalBindingMode bindingMode,
|
||||
}) {
|
||||
if (scenario is UniversalScenario) {
|
||||
if (scenario == UniversalScenario.asyncChain ||
|
||||
bindingMode == UniversalBindingMode.asyncStrategy) {
|
||||
throw UnsupportedError(
|
||||
'YxScope does not support async dependencies or async binding scenarios.');
|
||||
}
|
||||
return (scope) {
|
||||
switch (scenario) {
|
||||
case UniversalScenario.asyncChain:
|
||||
for (int chain = 1; chain <= chainCount; chain++) {
|
||||
for (int level = 1; level <= nestingDepth; level++) {
|
||||
final prevDepName = '${chain}_${level - 1}';
|
||||
final depName = '${chain}_$level';
|
||||
final dep = scope.dep<UniversalService>(
|
||||
() => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: level > 1
|
||||
? scope.depFor<UniversalService>(name: prevDepName).get
|
||||
: null,
|
||||
),
|
||||
name: depName,
|
||||
);
|
||||
scope.register<UniversalService>(dep, name: depName);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UniversalScenario.register:
|
||||
final dep = scope.dep<UniversalService>(
|
||||
() => UniversalServiceImpl(value: 'reg', dependency: null),
|
||||
@@ -113,6 +99,8 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
|
||||
case UniversalScenario.override:
|
||||
// handled at benchmark level
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (scenario == UniversalScenario.chain ||
|
||||
scenario == UniversalScenario.override) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/// Enum to represent the DI registration/binding mode.
|
||||
enum UniversalBindingMode {
|
||||
/// Singleton/provider binding.
|
||||
/// Eager singleton — instance created at registration time.
|
||||
singletonStrategy,
|
||||
|
||||
/// Factory-based binding.
|
||||
/// Lazy singleton — instance created on first resolve, then cached.
|
||||
lazySingletonStrategy,
|
||||
|
||||
/// Factory-based binding — new instance every time.
|
||||
factoryStrategy,
|
||||
|
||||
/// Async-based binding.
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
## 3.0.3
|
||||
|
||||
- **DOCS**: sync references after ProviderFactory rename.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **BREAKING** **REFACTOR**(binding): rename `Provider<T>` typedef to `ProviderFactory<T>` to avoid naming conflicts with `package:provider`.
|
||||
|
||||
## 3.0.2
|
||||
|
||||
- **FIX**(test): fix warning.
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cherrypick/src/binding_resolver.dart';
|
||||
|
||||
/// {@template binding_docs}
|
||||
@@ -69,6 +71,8 @@ class Binding<T> {
|
||||
|
||||
CherryPickObserver? observer;
|
||||
|
||||
bool get _isSilentObserver => observer is SilentCherryPickObserver;
|
||||
|
||||
// Deferred logging flags
|
||||
bool _createdLogged = false;
|
||||
bool _namedLogged = false;
|
||||
@@ -191,10 +195,9 @@ class Binding<T> {
|
||||
/// }
|
||||
/// ```
|
||||
/// This restriction only applies to [toInstance] bindings.
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
/// With [toProvide]/[toProvideAsync] you may freely use `scope.resolve<T>()` in the builder or provider function.
|
||||
Binding<T> toInstance(Instance<T> value) {
|
||||
_resolver = InstanceResolver<T>(value);
|
||||
Binding<T> toInstance(FutureOr<T> value) {
|
||||
_resolver = InstanceResolver.create<T>(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -205,8 +208,8 @@ class Binding<T> {
|
||||
/// bind<Api>().toProvide(() => ApiService());
|
||||
/// bind<Db>().toProvide(() async => await openDb());
|
||||
/// ```
|
||||
Binding<T> toProvide(ProviderFactory<T> value) {
|
||||
_resolver = ProviderResolver<T>((_) => value.call(), withParams: false);
|
||||
Binding<T> toProvide(FutureOr<T> Function() value) {
|
||||
_resolver = ProviderResolver.create<T>(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -216,24 +219,32 @@ class Binding<T> {
|
||||
/// ```dart
|
||||
/// bind<User>().toProvideWithParams((params) => User(name: params["name"]));
|
||||
/// ```
|
||||
Binding<T> toProvideWithParams(ProviderWithParams<T> value) {
|
||||
_resolver = ProviderResolver<T>(value, withParams: true);
|
||||
Binding<T> toProvideWithParams(FutureOr<T> Function(dynamic) value) {
|
||||
_resolver = ProviderResolver.createWithParams<T>(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated('Use toInstance instead of toInstanceAsync')
|
||||
Binding<T> toInstanceAsync(Instance<T> value) {
|
||||
Binding<T> toInstanceAsync(FutureOr<T> value) {
|
||||
return toInstance(value);
|
||||
}
|
||||
|
||||
@Deprecated('Use toProvide instead of toProvideAsync')
|
||||
Binding<T> toProvideAsync(ProviderFactory<T> value) {
|
||||
return toProvide(value);
|
||||
/// Asynchronous variant of [toProvide] for providers that return [Future<T>].
|
||||
///
|
||||
/// Prefer this over [toProvide] when the provider is async, so the resolver
|
||||
/// is type-safe and avoids runtime detection overhead.
|
||||
Binding<T> toProvideAsync(AsyncProviderFactory<T> value) {
|
||||
_resolver = ProviderResolver.async<T>(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated('Use toProvideWithParams instead of toProvideAsyncWithParams')
|
||||
Binding<T> toProvideAsyncWithParams(ProviderWithParams<T> value) {
|
||||
return toProvideWithParams(value);
|
||||
/// Asynchronous variant of [toProvideWithParams] for providers that return [Future<T>].
|
||||
///
|
||||
/// Prefer this over [toProvideWithParams] when the provider is async, so the resolver
|
||||
/// is type-safe and avoids runtime detection overhead.
|
||||
Binding<T> toProvideAsyncWithParams(AsyncProviderFactoryWithParams<T> value) {
|
||||
_resolver = ProviderResolver.asyncWithParams<T>(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Marks this binding as singleton (will only create and cache one instance per scope).
|
||||
@@ -281,6 +292,10 @@ class Binding<T> {
|
||||
/// ```
|
||||
T? resolveSync([dynamic params]) {
|
||||
final res = resolver?.resolveSync(params);
|
||||
if (_isSilentObserver) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (res != null) {
|
||||
observer?.onDiagnostic(
|
||||
'Binding resolved instance: ${T.toString()}',
|
||||
@@ -313,6 +328,10 @@ class Binding<T> {
|
||||
/// ```
|
||||
Future<T>? resolveAsync([dynamic params]) {
|
||||
final future = resolver?.resolveAsync(params);
|
||||
if (_isSilentObserver) {
|
||||
return future;
|
||||
}
|
||||
|
||||
if (future != null) {
|
||||
future
|
||||
.then((res) => observer?.onDiagnostic(
|
||||
|
||||
@@ -13,86 +13,37 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
/// Represents a direct instance or an async instance ([T] or [Future<T>]).
|
||||
/// Used for both direct and async bindings.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Instance<String> sync = "hello";
|
||||
/// Instance<MyApi> async = Future.value(MyApi());
|
||||
/// ```
|
||||
typedef Instance<T> = FutureOr<T>;
|
||||
/// Synchronous factory: `T Function()`.
|
||||
typedef ProviderFactory<T> = T Function();
|
||||
|
||||
/// Provider function type for synchronous or asynchronous, parameterless creation of [T].
|
||||
/// Can return [T] or [Future<T>].
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ProviderFactory<MyService> provider = () => MyService();
|
||||
/// ProviderFactory<Api> asyncProvider = () async => await Api.connect();
|
||||
/// ```
|
||||
typedef ProviderFactory<T> = FutureOr<T> Function();
|
||||
/// Parameterized synchronous factory: `T Function(dynamic)`.
|
||||
typedef ProviderFactoryWithParams<T> = T Function(dynamic);
|
||||
|
||||
/// Provider function type that accepts a dynamic parameter, for factory/parametrized injection.
|
||||
/// Returns [T] or [Future<T>].
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ProviderWithParams<User> provider = (params) => User(params["name"]);
|
||||
/// ```
|
||||
typedef ProviderWithParams<T> = FutureOr<T> Function(dynamic);
|
||||
/// Asynchronous factory: `Future<T> Function()`.
|
||||
typedef AsyncProviderFactory<T> = Future<T> Function();
|
||||
|
||||
/// Abstract interface for dependency resolvers used by [Binding].
|
||||
/// Defines how to resolve instances of type [T].
|
||||
///
|
||||
/// You usually don't use this directly; it's used internally for advanced/low-level DI.
|
||||
/// Parameterized asynchronous factory: `Future<T> Function(dynamic)`.
|
||||
typedef AsyncProviderFactoryWithParams<T> = Future<T> Function(dynamic);
|
||||
|
||||
/// Internal interface for resolvers managed by [Binding].
|
||||
abstract class BindingResolver<T> {
|
||||
/// Synchronously resolves the dependency, optionally taking parameters (for factory cases).
|
||||
/// Throws if implementation does not support sync resolution.
|
||||
T? resolveSync([dynamic params]);
|
||||
|
||||
/// Asynchronously resolves the dependency, optionally taking parameters (for factory cases).
|
||||
/// If instance is already a [Future], returns it directly.
|
||||
Future<T>? resolveAsync([dynamic params]);
|
||||
|
||||
/// Marks this resolver as singleton: instance(s) will be cached and reused inside the scope.
|
||||
void toSingleton();
|
||||
|
||||
/// Returns true if this resolver is marked as singleton.
|
||||
bool get isSingleton;
|
||||
}
|
||||
|
||||
/// Concrete resolver for direct instance ([T] or [Future<T>]). No provider is called.
|
||||
///
|
||||
/// Used for [Binding.toInstance].
|
||||
/// Supports both sync and async resolution; sync will throw if underlying instance is [Future].
|
||||
/// Examples:
|
||||
/// ```dart
|
||||
/// var resolver = InstanceResolver("hello");
|
||||
/// resolver.resolveSync(); // == "hello"
|
||||
/// var asyncResolver = InstanceResolver(Future.value(7));
|
||||
/// asyncResolver.resolveAsync(); // Future<int>
|
||||
/// ```
|
||||
class InstanceResolver<T> implements BindingResolver<T> {
|
||||
final Instance<T> _instance;
|
||||
/// Resolver for a pre-built synchronous instance.
|
||||
class SyncInstanceResolver<T> implements BindingResolver<T> {
|
||||
final T _instance;
|
||||
|
||||
/// Wraps the given instance (sync or async) in a resolver.
|
||||
InstanceResolver(this._instance);
|
||||
SyncInstanceResolver(this._instance);
|
||||
|
||||
@override
|
||||
T resolveSync([_]) {
|
||||
if (_instance is T) return _instance;
|
||||
throw StateError(
|
||||
'Instance $_instance is Future; '
|
||||
'use resolveAsync() instead',
|
||||
);
|
||||
}
|
||||
T resolveSync([_]) => _instance;
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([_]) {
|
||||
if (_instance is Future<T>) return _instance;
|
||||
return Future.value(_instance);
|
||||
}
|
||||
Future<T> resolveAsync([_]) => Future<T>.value(_instance);
|
||||
|
||||
@override
|
||||
void toSingleton() {}
|
||||
@@ -101,63 +52,31 @@ class InstanceResolver<T> implements BindingResolver<T> {
|
||||
bool get isSingleton => true;
|
||||
}
|
||||
|
||||
/// Resolver for provider functions (sync/async/factory), with optional singleton caching.
|
||||
/// Used for [Binding.toProvide], [Binding.toProvideWithParams], [Binding.singleton].
|
||||
///
|
||||
/// Examples:
|
||||
/// ```dart
|
||||
/// // No param, sync:
|
||||
/// var r = ProviderResolver((_) => 5, withParams: false);
|
||||
/// r.resolveSync(); // == 5
|
||||
/// // With param:
|
||||
/// var rp = ProviderResolver((p) => p * 2, withParams: true);
|
||||
/// rp.resolveSync(2); // == 4
|
||||
/// // Singleton:
|
||||
/// r.toSingleton();
|
||||
/// // Async:
|
||||
/// var ra = ProviderResolver((_) async => await Future.value(10), withParams: false);
|
||||
/// await ra.resolveAsync(); // == 10
|
||||
/// ```
|
||||
class ProviderResolver<T> implements BindingResolver<T> {
|
||||
final ProviderWithParams<T> _provider;
|
||||
final bool _withParams;
|
||||
/// Resolver for a pre-built async instance ([Future]).
|
||||
class AsyncInstanceResolver<T> implements BindingResolver<T> {
|
||||
final Future<T> _instance;
|
||||
|
||||
FutureOr<T>? _cache;
|
||||
AsyncInstanceResolver(this._instance);
|
||||
|
||||
@override
|
||||
T resolveSync([_]) {
|
||||
throw StateError('Instance is a Future; use resolveAsync() instead');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([_]) => _instance;
|
||||
|
||||
@override
|
||||
void toSingleton() {}
|
||||
|
||||
@override
|
||||
bool get isSingleton => true;
|
||||
}
|
||||
|
||||
/// Base class for provider-based resolvers with singleton flag support.
|
||||
abstract class _BaseProviderResolver<T> implements BindingResolver<T> {
|
||||
bool _singleton = false;
|
||||
|
||||
/// Creates a resolver from [provider], optionally accepting dynamic params.
|
||||
ProviderResolver(
|
||||
ProviderWithParams<T> provider, {
|
||||
required bool withParams,
|
||||
}) : _provider = provider,
|
||||
_withParams = withParams;
|
||||
|
||||
@override
|
||||
T resolveSync([dynamic params]) {
|
||||
_checkParams(params);
|
||||
final result = _cache ?? _provider(params);
|
||||
if (result is T) {
|
||||
if (_singleton) {
|
||||
_cache ??= result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
throw StateError(
|
||||
'Provider [$_provider] return Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([dynamic params]) {
|
||||
_checkParams(params);
|
||||
final result = _cache ?? _provider(params);
|
||||
final target = result is Future<T> ? result : Future<T>.value(result);
|
||||
if (_singleton) {
|
||||
_cache ??= target;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
@override
|
||||
void toSingleton() {
|
||||
_singleton = true;
|
||||
@@ -165,13 +84,282 @@ class ProviderResolver<T> implements BindingResolver<T> {
|
||||
|
||||
@override
|
||||
bool get isSingleton => _singleton;
|
||||
}
|
||||
|
||||
/// Throws if params required but not supplied.
|
||||
void _checkParams(dynamic params) {
|
||||
if (_withParams && params == null) {
|
||||
/// Resolves [Binding.toProvide] with a sync `T Function()` provider.
|
||||
class SyncProviderResolver<T> extends _BaseProviderResolver<T> {
|
||||
final ProviderFactory<T> _provider;
|
||||
Object? _cached;
|
||||
bool _isCached = false;
|
||||
|
||||
SyncProviderResolver(this._provider);
|
||||
|
||||
@override
|
||||
T resolveSync([_]) {
|
||||
if (_singleton && _isCached) {
|
||||
return _cached as T;
|
||||
}
|
||||
final result = _provider();
|
||||
if (_singleton) {
|
||||
_cached = result;
|
||||
_isCached = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([_]) => Future<T>.value(resolveSync());
|
||||
}
|
||||
|
||||
/// Resolves [Binding.toProvideWithParams] with a sync `T Function(dynamic)` provider.
|
||||
class SyncProviderWithParamsResolver<T> extends _BaseProviderResolver<T> {
|
||||
final ProviderFactoryWithParams<T> _provider;
|
||||
Object? _cached;
|
||||
bool _isCached = false;
|
||||
|
||||
SyncProviderWithParamsResolver(this._provider);
|
||||
|
||||
@override
|
||||
T resolveSync([dynamic params]) {
|
||||
if (_singleton && _isCached) {
|
||||
return _cached as T;
|
||||
}
|
||||
if (params == null) {
|
||||
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
|
||||
}
|
||||
final result = _provider(params);
|
||||
if (_singleton) {
|
||||
_cached = result;
|
||||
_isCached = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([dynamic params]) =>
|
||||
Future<T>.value(resolveSync(params));
|
||||
}
|
||||
|
||||
/// Resolves [Binding.toProvide] with an async `Future<T> Function()` provider.
|
||||
class AsyncProviderResolver<T> extends _BaseProviderResolver<T> {
|
||||
final AsyncProviderFactory<T> _provider;
|
||||
Future<T>? _cache;
|
||||
bool _isCached = false;
|
||||
|
||||
AsyncProviderResolver(this._provider);
|
||||
|
||||
@override
|
||||
T resolveSync([_]) {
|
||||
throw StateError(
|
||||
'[$T] Params is null. Maybe you forgot to pass it?',
|
||||
'Provider returns Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([_]) {
|
||||
if (_singleton && _isCached) {
|
||||
return _cache!;
|
||||
}
|
||||
final result = _provider();
|
||||
if (_singleton) {
|
||||
_cache = result;
|
||||
_isCached = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves [Binding.toProvideWithParams] with an async `Future<T> Function(dynamic)` provider.
|
||||
class AsyncProviderWithParamsResolver<T> extends _BaseProviderResolver<T> {
|
||||
final AsyncProviderFactoryWithParams<T> _provider;
|
||||
Future<T>? _cache;
|
||||
bool _isCached = false;
|
||||
|
||||
AsyncProviderWithParamsResolver(this._provider);
|
||||
|
||||
@override
|
||||
T resolveSync([_]) {
|
||||
throw StateError(
|
||||
'Provider returns Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([dynamic params]) {
|
||||
if (_singleton && _isCached) {
|
||||
return _cache!;
|
||||
}
|
||||
if (params == null) {
|
||||
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
|
||||
}
|
||||
final result = _provider(params);
|
||||
if (_singleton) {
|
||||
_cache = result;
|
||||
_isCached = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback resolver for `FutureOr<T> Function()` providers whose static type
|
||||
/// is not known at compile time. Detects sync vs async at resolve time.
|
||||
class FutureOrProviderResolver<T> extends _BaseProviderResolver<T> {
|
||||
final FutureOr<T> Function() _provider;
|
||||
Object? _cached;
|
||||
bool _isCached = false;
|
||||
|
||||
FutureOrProviderResolver(this._provider);
|
||||
|
||||
@override
|
||||
T resolveSync([_]) {
|
||||
if (_singleton && _isCached) {
|
||||
final cached = _cached;
|
||||
if (cached is Future<T>) {
|
||||
throw StateError(
|
||||
'Provider returns Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
}
|
||||
return cached as T;
|
||||
}
|
||||
final result = _provider();
|
||||
if (result is Future<T>) {
|
||||
throw StateError(
|
||||
'Provider returns Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
}
|
||||
if (_singleton) {
|
||||
_cached = result;
|
||||
_isCached = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([_]) {
|
||||
if (_singleton && _isCached) {
|
||||
final cached = _cached;
|
||||
if (cached is Future<T>) return cached;
|
||||
return Future<T>.value(cached as T);
|
||||
}
|
||||
final result = _provider();
|
||||
if (_singleton) {
|
||||
_cached = result;
|
||||
_isCached = true;
|
||||
}
|
||||
if (result is Future<T>) return result;
|
||||
return Future<T>.value(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback resolver for `FutureOr<T> Function(dynamic)` providers with params
|
||||
/// whose static type is not known at compile time.
|
||||
class FutureOrProviderWithParamsResolver<T> extends _BaseProviderResolver<T> {
|
||||
final FutureOr<T> Function(dynamic) _provider;
|
||||
Object? _cached;
|
||||
bool _isCached = false;
|
||||
|
||||
FutureOrProviderWithParamsResolver(this._provider);
|
||||
|
||||
@override
|
||||
T resolveSync([dynamic params]) {
|
||||
if (_singleton && _isCached) {
|
||||
final cached = _cached;
|
||||
if (cached is Future<T>) {
|
||||
throw StateError(
|
||||
'Provider returns Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
}
|
||||
return cached as T;
|
||||
}
|
||||
if (params == null) {
|
||||
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
|
||||
}
|
||||
final result = _provider(params);
|
||||
if (result is Future<T>) {
|
||||
throw StateError(
|
||||
'Provider returns Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
}
|
||||
if (_singleton) {
|
||||
_cached = result;
|
||||
_isCached = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> resolveAsync([dynamic params]) {
|
||||
if (_singleton && _isCached) {
|
||||
final cached = _cached;
|
||||
if (cached is Future<T>) return cached;
|
||||
return Future<T>.value(cached as T);
|
||||
}
|
||||
if (params == null) {
|
||||
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
|
||||
}
|
||||
final result = _provider(params);
|
||||
if (_singleton) {
|
||||
_cached = result;
|
||||
_isCached = true;
|
||||
}
|
||||
if (result is Future<T>) return result;
|
||||
return Future<T>.value(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory for creating instance resolvers (sync or async).
|
||||
class InstanceResolver {
|
||||
static BindingResolver<T> create<T>(FutureOr<T> instance) {
|
||||
if (instance is Future<T>) {
|
||||
return AsyncInstanceResolver<T>(instance);
|
||||
}
|
||||
return SyncInstanceResolver<T>(instance);
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory for creating the correct provider resolver based on the
|
||||
/// provider's static return type, avoiding runtime checks in fast paths.
|
||||
class ProviderResolver {
|
||||
static BindingResolver<T> create<T>(FutureOr<T> Function() provider) {
|
||||
if (provider is T Function()) {
|
||||
return SyncProviderResolver<T>(provider);
|
||||
}
|
||||
if (provider is Future<T> Function()) {
|
||||
return AsyncProviderResolver<T>(provider);
|
||||
}
|
||||
return FutureOrProviderResolver<T>(provider);
|
||||
}
|
||||
|
||||
static BindingResolver<T> createWithParams<T>(
|
||||
FutureOr<T> Function(dynamic) provider) {
|
||||
if (provider is T Function(dynamic)) {
|
||||
return SyncProviderWithParamsResolver<T>(provider);
|
||||
}
|
||||
if (provider is Future<T> Function(dynamic)) {
|
||||
return AsyncProviderWithParamsResolver<T>(provider);
|
||||
}
|
||||
return FutureOrProviderWithParamsResolver<T>(provider);
|
||||
}
|
||||
|
||||
/// Explicit sync resolver without parameters.
|
||||
static BindingResolver<T> sync<T>(ProviderFactory<T> provider) {
|
||||
return SyncProviderResolver<T>(provider);
|
||||
}
|
||||
|
||||
/// Explicit sync resolver with parameters.
|
||||
static BindingResolver<T> syncWithParams<T>(
|
||||
ProviderFactoryWithParams<T> provider) {
|
||||
return SyncProviderWithParamsResolver<T>(provider);
|
||||
}
|
||||
|
||||
/// Explicit async resolver without parameters.
|
||||
static BindingResolver<T> async<T>(AsyncProviderFactory<T> provider) {
|
||||
return AsyncProviderResolver<T>(provider);
|
||||
}
|
||||
|
||||
/// Explicit async resolver with parameters.
|
||||
static BindingResolver<T> asyncWithParams<T>(
|
||||
AsyncProviderFactoryWithParams<T> provider) {
|
||||
return AsyncProviderWithParamsResolver<T>(provider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
import 'package:cherrypick/src/binding_resolver.dart';
|
||||
import 'package:cherrypick/src/module.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
// import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
/// Represents a DI scope (container) for modules, subscopes,
|
||||
/// and dependency resolution (sync/async) in CherryPick.
|
||||
@@ -60,6 +59,13 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
@override
|
||||
CherryPickObserver get observer => _observer;
|
||||
|
||||
bool get _isSilentObserver => _observer is SilentCherryPickObserver;
|
||||
|
||||
bool get _canUseDirectResolvePath =>
|
||||
_isSilentObserver &&
|
||||
!isCycleDetectionEnabled &&
|
||||
!isGlobalCycleDetectionEnabled;
|
||||
|
||||
/// COLLECTS all resolved instances that implement [Disposable].
|
||||
final Set<Disposable> _disposables = HashSet();
|
||||
|
||||
@@ -71,6 +77,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
Scope(this._parentScope, {required CherryPickObserver observer})
|
||||
: _observer = observer {
|
||||
setScopeId(_generateScopeId());
|
||||
if (!_isSilentObserver) {
|
||||
observer.onScopeOpened(scopeId ?? 'NO_ID');
|
||||
observer.onDiagnostic(
|
||||
'Scope created: ${scopeId ?? 'NO_ID'}',
|
||||
@@ -82,10 +89,11 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final Set<Module> _modulesList = HashSet();
|
||||
|
||||
// индекс для мгновенного поиска binding’ов
|
||||
// index for fast binding lookup
|
||||
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
|
||||
|
||||
/// Generates a unique identifier string for this scope instance.
|
||||
@@ -119,6 +127,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
childScope.enableGlobalCycleDetection();
|
||||
}
|
||||
_scopeMap[name] = childScope;
|
||||
if (!_isSilentObserver) {
|
||||
observer.onDiagnostic(
|
||||
'SubScope created: $name',
|
||||
details: {
|
||||
@@ -130,6 +139,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
|
||||
@@ -149,6 +159,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
}
|
||||
if (!_isSilentObserver) {
|
||||
observer.onScopeClosed(childScope.scopeId ?? name);
|
||||
observer.onDiagnostic(
|
||||
'SubScope closed: $name',
|
||||
@@ -161,6 +172,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
_scopeMap.remove(name);
|
||||
}
|
||||
|
||||
@@ -175,13 +187,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// ```
|
||||
Scope installModules(List<Module> modules) {
|
||||
_modulesList.addAll(modules);
|
||||
if (modules.isNotEmpty) {
|
||||
if (!_isSilentObserver && modules.isNotEmpty) {
|
||||
observer.onModulesInstalled(
|
||||
modules.map((m) => m.runtimeType.toString()).toList(),
|
||||
scopeName: scopeId,
|
||||
);
|
||||
}
|
||||
for (var module in modules) {
|
||||
if (!_isSilentObserver) {
|
||||
observer.onDiagnostic(
|
||||
'Module installed: ${module.runtimeType}',
|
||||
details: {
|
||||
@@ -191,14 +204,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
'description': 'module installed',
|
||||
},
|
||||
);
|
||||
}
|
||||
module.builder(this);
|
||||
// Associate bindings with this scope's observer
|
||||
for (final binding in module.bindingSet) {
|
||||
binding.observer = observer;
|
||||
if (!_isSilentObserver) {
|
||||
binding.logAllDeferred();
|
||||
}
|
||||
}
|
||||
_rebuildResolversIndex();
|
||||
_addModuleToIndex(module);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -212,12 +228,13 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// testScope.dropModules();
|
||||
/// ```
|
||||
Scope dropModules() {
|
||||
if (_modulesList.isNotEmpty) {
|
||||
if (!_isSilentObserver && _modulesList.isNotEmpty) {
|
||||
observer.onModulesRemoved(
|
||||
_modulesList.map((m) => m.runtimeType.toString()).toList(),
|
||||
scopeName: scopeId,
|
||||
);
|
||||
}
|
||||
if (!_isSilentObserver) {
|
||||
observer.onDiagnostic(
|
||||
'Modules dropped for scope: $scopeId',
|
||||
details: {
|
||||
@@ -226,6 +243,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
'description': 'modules dropped',
|
||||
},
|
||||
);
|
||||
}
|
||||
_modulesList.clear();
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
@@ -242,6 +260,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final special = scope.resolve<Service>(named: 'special');
|
||||
/// ```
|
||||
T resolve<T>({String? named, dynamic params}) {
|
||||
if (_canUseDirectResolvePath) {
|
||||
final result = _tryResolveInternal<T>(named: named, params: params);
|
||||
if (result == null) {
|
||||
throw StateError(
|
||||
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
||||
}
|
||||
if (result is Disposable) _disposables.add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
observer.onInstanceRequested(T.toString(), T, scopeName: scopeId);
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
@@ -315,6 +343,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final maybeDb = scope.tryResolve<Database>();
|
||||
/// ```
|
||||
T? tryResolve<T>({String? named, dynamic params}) {
|
||||
if (_canUseDirectResolvePath) {
|
||||
final result = _tryResolveInternal<T>(named: named, params: params);
|
||||
if (result != null && result is Disposable) _disposables.add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
T? result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = withGlobalCycleDetection<T?>(T, named, () {
|
||||
@@ -358,6 +392,21 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final special = await scope.resolveAsync<Service>(named: "special");
|
||||
/// ```
|
||||
Future<T> resolveAsync<T>({String? named, dynamic params}) async {
|
||||
if (_canUseDirectResolvePath) {
|
||||
final result =
|
||||
await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||
if (result == null) {
|
||||
throw StateError(
|
||||
"Can't resolve async dependency `$T`. Maybe you forget register it?");
|
||||
}
|
||||
if (result is Disposable) _disposables.add(result);
|
||||
return result;
|
||||
}
|
||||
return _resolveAsyncWithObserverPath<T>(named: named, params: params);
|
||||
}
|
||||
|
||||
Future<T> _resolveAsyncWithObserverPath<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
|
||||
@@ -413,6 +462,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final user = await scope.tryResolveAsync<User>();
|
||||
/// ```
|
||||
Future<T?> tryResolveAsync<T>({String? named, dynamic params}) async {
|
||||
if (_canUseDirectResolvePath) {
|
||||
final result =
|
||||
await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||
if (result != null && result is Disposable) _disposables.add(result);
|
||||
return result;
|
||||
}
|
||||
return _tryResolveAsyncWithObserverPath<T>(named: named, params: params);
|
||||
}
|
||||
|
||||
Future<T?> _tryResolveAsyncWithObserverPath<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
T? result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = await withGlobalCycleDetection<Future<T?>>(T, named, () async {
|
||||
@@ -441,12 +501,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
}
|
||||
|
||||
/// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only.
|
||||
Future<T?> _tryResolveAsyncInternal<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
Future<T?> _tryResolveAsyncInternal<T>({String? named, dynamic params}) {
|
||||
final resolver = _findBindingResolver<T>(named);
|
||||
// 1 - Try from own modules; 2 - Fallback to parent
|
||||
return resolver?.resolveAsync(params) ??
|
||||
_parentScope?.tryResolveAsync(named: named, params: params);
|
||||
_parentScope?.tryResolveAsync(named: named, params: params) ??
|
||||
Future<T?>.value(null);
|
||||
}
|
||||
|
||||
/// Looks up the [BindingResolver] for [T] and [named] within this scope.
|
||||
@@ -454,23 +514,27 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
BindingResolver<T>? _findBindingResolver<T>(String? named) =>
|
||||
_bindingResolvers[T]?[named] as BindingResolver<T>?;
|
||||
|
||||
/// Rebuilds the internal index of all [BindingResolver]s from installed modules.
|
||||
/// Called after [installModules] and [dropModules]. Internal use only.
|
||||
void _rebuildResolversIndex() {
|
||||
_bindingResolvers.clear();
|
||||
for (var module in _modulesList) {
|
||||
void _addModuleToIndex(Module module) {
|
||||
for (var binding in module.bindingSet) {
|
||||
_bindingResolvers.putIfAbsent(binding.key, () => {});
|
||||
final nameKey = binding.isNamed ? binding.name : null;
|
||||
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds the internal index of all [BindingResolver]s from installed modules.
|
||||
/// Called after [dropModules]. Internal use only.
|
||||
void _rebuildResolversIndex() {
|
||||
_bindingResolvers.clear();
|
||||
for (var module in _modulesList) {
|
||||
_addModuleToIndex(module);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks resolved [Disposable] instances, to ensure dispose is called automatically.
|
||||
/// Internal use only.
|
||||
void _trackDisposable(Object? obj) {
|
||||
if (obj is Disposable && !_disposables.contains(obj)) {
|
||||
if (obj is Disposable) {
|
||||
_disposables.add(obj);
|
||||
}
|
||||
}
|
||||
@@ -487,14 +551,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// ```
|
||||
Future<void> dispose() async {
|
||||
// Create copies to avoid concurrent modification
|
||||
final scopesCopy = Map<String, Scope>.from(_scopeMap);
|
||||
for (final subScope in scopesCopy.values) {
|
||||
final scopes = _scopeMap.values.toList();
|
||||
for (final subScope in scopes) {
|
||||
await subScope.dispose();
|
||||
}
|
||||
_scopeMap.clear();
|
||||
|
||||
final disposablesCopy = Set<Disposable>.from(_disposables);
|
||||
for (final d in disposablesCopy) {
|
||||
final disposables = _disposables.toList();
|
||||
for (final d in disposables) {
|
||||
await d.dispose();
|
||||
}
|
||||
_disposables.clear();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: cherrypick
|
||||
description: Cherrypick is a small dependency injection (DI) library for dart/flutter projects.
|
||||
version: 3.0.3
|
||||
version: 3.0.2
|
||||
homepage: https://cherrypick-di.netlify.app
|
||||
documentation: https://cherrypick-di.netlify.app/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@@ -12,7 +14,8 @@ void main() {
|
||||
|
||||
test('Sets mode to instance', () {
|
||||
final binding = Binding<int>().toInstance(5);
|
||||
expect(binding.resolver, isA<InstanceResolver<int>>());
|
||||
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
|
||||
});
|
||||
|
||||
test('isSingleton is true', () {
|
||||
@@ -34,7 +37,8 @@ void main() {
|
||||
|
||||
test('Sets mode to instance', () {
|
||||
final binding = Binding<int>().withName('n').toInstance(5);
|
||||
expect(binding.resolver, isA<InstanceResolver<int>>());
|
||||
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
|
||||
});
|
||||
|
||||
test('Sets key', () {
|
||||
@@ -73,7 +77,8 @@ void main() {
|
||||
|
||||
test('Sets mode to instance', () {
|
||||
final binding = Binding<int>().toInstance(Future.value(5));
|
||||
expect(binding.resolver, isA<InstanceResolver<int>>());
|
||||
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||
expect(binding.resolver, isA<AsyncInstanceResolver<int>>());
|
||||
});
|
||||
|
||||
test('isSingleton is true after toInstanceAsync', () {
|
||||
@@ -107,7 +112,8 @@ void main() {
|
||||
|
||||
test('Sets mode to providerInstance', () {
|
||||
final binding = Binding<int>().toProvide(() => 5);
|
||||
expect(binding.resolver, isA<ProviderResolver<int>>());
|
||||
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||
expect(binding.resolveSync(), 5);
|
||||
});
|
||||
|
||||
test('isSingleton is false by default', () {
|
||||
@@ -129,7 +135,8 @@ void main() {
|
||||
|
||||
test('Sets mode to providerInstance', () {
|
||||
final binding = Binding<int>().withName('n').toProvide(() => 5);
|
||||
expect(binding.resolver, isA<ProviderResolver<int>>());
|
||||
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||
expect(binding.resolveSync(), 5);
|
||||
});
|
||||
|
||||
test('Sets key', () {
|
||||
@@ -166,6 +173,43 @@ void main() {
|
||||
.toProvideWithParams((param) async => 5 + (param as int));
|
||||
expect(await binding.resolveAsync(3), 8);
|
||||
});
|
||||
|
||||
test('Resolves toProvideAsync value', () async {
|
||||
final binding = Binding<int>().toProvideAsync(() async => 5);
|
||||
expect(await binding.resolveAsync(), 5);
|
||||
});
|
||||
|
||||
test('toProvideAsync singleton caches instance', () async {
|
||||
int counter = 0;
|
||||
final binding = Binding<int>().toProvideAsync(() async {
|
||||
counter++;
|
||||
return counter;
|
||||
}).singleton();
|
||||
|
||||
final first = await binding.resolveAsync();
|
||||
final second = await binding.resolveAsync();
|
||||
expect(first, equals(second));
|
||||
expect(counter, 1);
|
||||
});
|
||||
|
||||
test('Resolves toProvideAsyncWithParams value', () async {
|
||||
final binding = Binding<int>()
|
||||
.toProvideAsyncWithParams((param) async => 5 + (param as int));
|
||||
expect(await binding.resolveAsync(3), 8);
|
||||
});
|
||||
|
||||
test('toProvideAsyncWithParams singleton caches instance', () async {
|
||||
int counter = 0;
|
||||
final binding = Binding<int>().toProvideAsyncWithParams((param) async {
|
||||
counter++;
|
||||
return counter + (param as int);
|
||||
}).singleton();
|
||||
|
||||
final first = await binding.resolveAsync(10);
|
||||
final second = await binding.resolveAsync(20);
|
||||
expect(first, equals(second));
|
||||
expect(counter, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Singleton provider binding ---
|
||||
@@ -232,6 +276,103 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
// --- FutureOr provider resolver (lazy detection, no eager side-effects) ---
|
||||
group('FutureOr Provider Resolver', () {
|
||||
test('Does not call provider at binding creation', () {
|
||||
int calls = 0;
|
||||
FutureOr<int> provider() {
|
||||
calls++;
|
||||
return 42;
|
||||
}
|
||||
|
||||
final binding = Binding<int>().toProvide(provider);
|
||||
expect(calls, 0);
|
||||
expect(binding.resolveSync(), 42);
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
test('Sync FutureOr provider resolves via resolveSync', () {
|
||||
final binding = Binding<int>().toProvide(() => 7);
|
||||
expect(binding.resolveSync(), 7);
|
||||
});
|
||||
|
||||
test('Async FutureOr provider throws on resolveSync', () {
|
||||
final binding =
|
||||
Binding<int>().toProvide(() async => 7);
|
||||
expect(() => binding.resolveSync(), throwsStateError);
|
||||
});
|
||||
|
||||
test('Async FutureOr provider resolves via resolveAsync', () async {
|
||||
final binding =
|
||||
Binding<int>().toProvide(() async => 7);
|
||||
expect(await binding.resolveAsync(), 7);
|
||||
});
|
||||
|
||||
test('FutureOr provider with params does not call provider at creation',
|
||||
() {
|
||||
int calls = 0;
|
||||
FutureOr<int> provider(dynamic p) {
|
||||
calls++;
|
||||
return (p as int) + 1;
|
||||
}
|
||||
|
||||
final binding = Binding<int>().toProvideWithParams(provider);
|
||||
expect(calls, 0);
|
||||
expect(binding.resolveSync(5), 6);
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
test('Async FutureOr provider with params throws on resolveSync', () {
|
||||
final binding = Binding<int>()
|
||||
.toProvideWithParams((p) async => (p as int) + 1);
|
||||
expect(() => binding.resolveSync(5), throwsStateError);
|
||||
});
|
||||
|
||||
test('Async FutureOr provider with params resolves via resolveAsync',
|
||||
() async {
|
||||
final binding = Binding<int>()
|
||||
.toProvideWithParams((p) async => (p as int) + 1);
|
||||
expect(await binding.resolveAsync(5), 6);
|
||||
});
|
||||
|
||||
test('FutureOr singleton caches sync result', () {
|
||||
int calls = 0;
|
||||
final binding = Binding<int>().toProvide(() {
|
||||
calls++;
|
||||
return 99;
|
||||
}).singleton();
|
||||
|
||||
expect(binding.resolveSync(), 99);
|
||||
expect(binding.resolveSync(), 99);
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
test('FutureOr singleton caches async result', () async {
|
||||
int calls = 0;
|
||||
final binding = Binding<int>().toProvide(() async {
|
||||
calls++;
|
||||
return 99;
|
||||
}).singleton();
|
||||
|
||||
expect(await binding.resolveAsync(), 99);
|
||||
expect(await binding.resolveAsync(), 99);
|
||||
expect(calls, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// --- InstanceResolver factory ---
|
||||
group('InstanceResolver.create', () {
|
||||
test('Returns SyncInstanceResolver for sync value', () {
|
||||
final binding = Binding<int>().toInstance(5);
|
||||
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
|
||||
});
|
||||
|
||||
test('Returns AsyncInstanceResolver for Future value', () {
|
||||
final binding = Binding<int>().toInstance(Future.value(5));
|
||||
expect(binding.resolver, isA<AsyncInstanceResolver<int>>());
|
||||
});
|
||||
});
|
||||
|
||||
// --- WithName / Named binding, isNamed, edge-cases ---
|
||||
group('Named binding & helpers', () {
|
||||
test('withName sets isNamed true and stores name', () {
|
||||
|
||||
153
cherrypick/test/src/scope_fast_path_test.dart
Normal file
153
cherrypick/test/src/scope_fast_path_test.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _IntModule extends Module {
|
||||
final int value;
|
||||
final void Function()? onBuild;
|
||||
|
||||
_IntModule(this.value, {this.onBuild});
|
||||
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
onBuild?.call();
|
||||
bind<int>().toInstance(value);
|
||||
}
|
||||
}
|
||||
|
||||
class _StringModule extends Module {
|
||||
final String value;
|
||||
|
||||
_StringModule(this.value);
|
||||
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<String>().toInstance(value);
|
||||
}
|
||||
}
|
||||
|
||||
class _NamedIntModule extends Module {
|
||||
final String name;
|
||||
final int value;
|
||||
|
||||
_NamedIntModule(this.name, this.value);
|
||||
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<int>().withName(name).toInstance(value);
|
||||
}
|
||||
}
|
||||
|
||||
class _DisposableModule extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<_DisposableService>().toProvide(() => _DisposableService());
|
||||
}
|
||||
}
|
||||
|
||||
class _DisposableService implements Disposable {
|
||||
bool disposed = false;
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
tearDown(() => CherryPick.closeRootScope());
|
||||
|
||||
group('SilentCherryPickObserver fast-path', () {
|
||||
late Scope scope;
|
||||
|
||||
setUp(() {
|
||||
// Must NOT have cycle detection — otherwise _canUseDirectResolvePath is false
|
||||
// and the fast-path branch is never taken.
|
||||
CherryPick.disableGlobalCycleDetection();
|
||||
scope = CherryPick.openRootScope();
|
||||
});
|
||||
|
||||
test('resolve with default silent observer uses fast-path', () {
|
||||
scope.installModules([_IntModule(42)]);
|
||||
expect(scope.resolve<int>(), 42);
|
||||
});
|
||||
|
||||
test('resolveAsync with default silent observer uses fast-path', () async {
|
||||
scope.installModules([_IntModule(42)]);
|
||||
final result = await scope.resolveAsync<int>();
|
||||
expect(result, 42);
|
||||
});
|
||||
|
||||
test('tryResolve with default silent observer uses fast-path', () {
|
||||
scope.installModules([_IntModule(42)]);
|
||||
expect(scope.tryResolve<int>(), 42);
|
||||
});
|
||||
|
||||
test('tryResolveAsync with default silent observer uses fast-path',
|
||||
() async {
|
||||
scope.installModules([_IntModule(42)]);
|
||||
final result = await scope.tryResolveAsync<int>();
|
||||
expect(result, 42);
|
||||
});
|
||||
|
||||
test('fast-path missing dependency throws', () {
|
||||
expect(() => scope.resolve<int>(), throwsStateError);
|
||||
});
|
||||
|
||||
test('fast-path missing async dependency throws', () {
|
||||
expect(scope.resolveAsync<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
|
||||
test('installModules with silent observer skips diagnostics', () {
|
||||
var buildCalls = 0;
|
||||
scope.installModules([_IntModule(42, onBuild: () => buildCalls++)]);
|
||||
expect(buildCalls, 1);
|
||||
expect(scope.resolve<int>(), 42);
|
||||
});
|
||||
|
||||
test('Disposable is tracked even in silent observer fast-path', () async {
|
||||
scope.installModules([_DisposableModule()]);
|
||||
|
||||
final service = scope.resolve<_DisposableService>();
|
||||
expect(service.disposed, false);
|
||||
await scope.dispose();
|
||||
expect(service.disposed, true);
|
||||
});
|
||||
});
|
||||
|
||||
group('Incremental module index', () {
|
||||
test('Multiple installModules calls accumulate bindings', () {
|
||||
final scope = CherryPick.openSafeRootScope();
|
||||
scope.installModules([_IntModule(1)]);
|
||||
scope.installModules([_StringModule('two')]);
|
||||
|
||||
expect(scope.resolve<int>(), 1);
|
||||
expect(scope.resolve<String>(), 'two');
|
||||
});
|
||||
|
||||
test('dropModules clears all bindings', () {
|
||||
final scope = CherryPick.openSafeRootScope();
|
||||
scope.installModules([_IntModule(1)]);
|
||||
scope.dropModules();
|
||||
|
||||
expect(() => scope.resolve<int>(), throwsStateError);
|
||||
});
|
||||
|
||||
test('Re-installing modules after dropModules works', () {
|
||||
final scope = CherryPick.openSafeRootScope();
|
||||
scope.installModules([_IntModule(1)]);
|
||||
scope.dropModules();
|
||||
scope.installModules([_IntModule(2)]);
|
||||
|
||||
expect(scope.resolve<int>(), 2);
|
||||
});
|
||||
|
||||
test('Named bindings are indexed incrementally', () {
|
||||
final scope = CherryPick.openSafeRootScope();
|
||||
scope.installModules([_NamedIntModule('a', 1)]);
|
||||
scope.installModules([_NamedIntModule('b', 2)]);
|
||||
|
||||
expect(scope.resolve<int>(named: 'a'), 1);
|
||||
expect(scope.resolve<int>(named: 'b'), 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
## 3.0.3
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 3.0.2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: cherrypick_flutter
|
||||
description: "Flutter library that allows access to the root scope through the context using `CherryPickProvider`."
|
||||
version: 3.0.3
|
||||
version: 3.0.2
|
||||
homepage: https://cherrypick-di.netlify.app
|
||||
documentation: https://cherrypick-di.netlify.app/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick
|
||||
@@ -19,7 +19,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cherrypick: ^3.0.3
|
||||
cherrypick: ^3.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
## 4.0.0-dev.1
|
||||
|
||||
- **REFACTOR**(cherrypick_generator): migrate inject codegen to code_builder.
|
||||
- **FIX**: add deps.
|
||||
|
||||
## 4.0.0-dev.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
@@ -11,17 +11,14 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element2.dart';
|
||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:code_builder/code_builder.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
|
||||
|
||||
import 'src/annotation_validator.dart';
|
||||
import 'src/code_builder_emitters.dart';
|
||||
import 'src/type_parser.dart';
|
||||
|
||||
/// CherryPick DI field injector generator for codegen.
|
||||
///
|
||||
/// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`)
|
||||
@@ -118,54 +115,24 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
final className = classElement.firstFragment.name2;
|
||||
final mixinName = '_\$$className';
|
||||
|
||||
AnnotationValidator.validateClassAnnotations(classElement);
|
||||
|
||||
final classType = TypeParser.parseType(classElement.thisType, classElement);
|
||||
final buffer = StringBuffer()
|
||||
..writeln('mixin $mixinName {')
|
||||
..writeln(' void _inject($className instance) {');
|
||||
|
||||
// Collect and process all @inject fields
|
||||
final injectFields = classElement.fields2
|
||||
.where((f) => _isInjectField(f))
|
||||
.map(_parseInjectField)
|
||||
.toList();
|
||||
.map((f) => _parseInjectField(f));
|
||||
|
||||
final injectMethod = Method((b) {
|
||||
b
|
||||
..name = '_inject'
|
||||
..returns = refer('void')
|
||||
..requiredParameters.add(
|
||||
Parameter((p) {
|
||||
p
|
||||
..name = 'instance'
|
||||
..type = CodeBuilderEmitters.resolveTypeRef(classType);
|
||||
}),
|
||||
)
|
||||
..body = Block((body) {
|
||||
for (final field in injectFields) {
|
||||
final scopeExpr = CodeBuilderEmitters.openScope(
|
||||
scopeName: field.scopeName,
|
||||
);
|
||||
final resolveExpr = CodeBuilderEmitters.resolveCall(
|
||||
scopeExpr: scopeExpr,
|
||||
parsedType: field.parsedType,
|
||||
named: field.namedValue,
|
||||
);
|
||||
body.statements.add(
|
||||
refer(
|
||||
'instance',
|
||||
).property(field.fieldName).assign(resolveExpr).statement,
|
||||
);
|
||||
for (final parsedField in injectFields) {
|
||||
buffer.writeln(_generateInjectionLine(parsedField));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
final mixin = Mixin((b) {
|
||||
b
|
||||
..name = mixinName
|
||||
..methods.add(injectMethod);
|
||||
});
|
||||
buffer
|
||||
..writeln(' }')
|
||||
..writeln('}');
|
||||
|
||||
final library = Library((b) => b..body.add(mixin));
|
||||
final emitter = DartEmitter(useNullSafetySyntax: true);
|
||||
return '${library.accept(emitter)}';
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Returns true if a field is annotated with `@inject`.
|
||||
@@ -183,13 +150,11 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
/// Converts Dart field declaration and all parameterizing injection-related
|
||||
/// annotations into a [_ParsedInjectField] which is used for codegen.
|
||||
static _ParsedInjectField _parseInjectField(FieldElement2 field) {
|
||||
AnnotationValidator.validateFieldAnnotations(field);
|
||||
|
||||
String? scopeName;
|
||||
String? namedValue;
|
||||
|
||||
for (final meta in field.firstFragment.metadata2.annotations) {
|
||||
final obj = meta.computeConstantValue();
|
||||
final DartObject? obj = meta.computeConstantValue();
|
||||
final type = obj?.type?.getDisplayString();
|
||||
if (type == 'scope') {
|
||||
scopeName = obj?.getField('name')?.toStringValue();
|
||||
@@ -199,15 +164,65 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
}
|
||||
|
||||
final DartType dartType = field.type;
|
||||
final parsedType = TypeParser.parseType(dartType, field);
|
||||
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;
|
||||
}
|
||||
|
||||
// Determine nullability for field types like T? or Future<T?>
|
||||
bool isNullable =
|
||||
dartType.nullabilitySuffix == NullabilitySuffix.question ||
|
||||
(dartType is ParameterizedType &&
|
||||
(dartType).typeArguments.any(
|
||||
(t) => t.nullabilitySuffix == NullabilitySuffix.question,
|
||||
));
|
||||
|
||||
return _ParsedInjectField(
|
||||
fieldName: field.firstFragment.name2 ?? '',
|
||||
parsedType: parsedType,
|
||||
coreType: coreTypeName.replaceAll('?', ''), // удаляем "?" на всякий
|
||||
isFuture: isFuture,
|
||||
isNullable: isNullable,
|
||||
scopeName: scopeName,
|
||||
namedValue: namedValue,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates Dart code for a single dependency-injected field based on its metadata.
|
||||
///
|
||||
/// This code will resolve the field from the CherryPick DI container and assign it to the class instance.
|
||||
/// Correctly dispatches to resolve, tryResolve, resolveAsync, or tryResolveAsync methods,
|
||||
/// and applies container scoping or named resolution where required.
|
||||
///
|
||||
/// Returns literal Dart code as string (1 line).
|
||||
///
|
||||
/// Example output:
|
||||
/// `instance.logger = CherryPick.openRootScope().resolve<Logger>();`
|
||||
String _generateInjectionLine(_ParsedInjectField field) {
|
||||
final resolveMethod = field.isFuture
|
||||
? (field.isNullable
|
||||
? 'tryResolveAsync<${field.coreType}>'
|
||||
: 'resolveAsync<${field.coreType}>')
|
||||
: (field.isNullable
|
||||
? 'tryResolve<${field.coreType}>'
|
||||
: 'resolve<${field.coreType}>');
|
||||
|
||||
final openCall = (field.scopeName != null && field.scopeName!.isNotEmpty)
|
||||
? "CherryPick.openScope(scopeName: '${field.scopeName}')"
|
||||
: "CherryPick.openRootScope()";
|
||||
|
||||
final params = (field.namedValue != null && field.namedValue!.isNotEmpty)
|
||||
? "(named: '${field.namedValue}')"
|
||||
: '()';
|
||||
|
||||
return " instance.${field.fieldName} = $openCall.$resolveMethod$params;";
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal structure: describes all required information for generating the injection
|
||||
@@ -218,8 +233,14 @@ class _ParsedInjectField {
|
||||
/// The name of the field to be injected.
|
||||
final String fieldName;
|
||||
|
||||
/// Parsed type info for the field.
|
||||
final ParsedType parsedType;
|
||||
/// The Dart type to resolve (e.g. `Logger` from `Logger` or `Future<Logger>`).
|
||||
final String coreType;
|
||||
|
||||
/// True if the field is an async dependency (Future<...>), otherwise false.
|
||||
final bool isFuture;
|
||||
|
||||
/// True if the field accepts null (T?), otherwise false.
|
||||
final bool isNullable;
|
||||
|
||||
/// The scoping for DI resolution, or null to use root scope.
|
||||
final String? scopeName;
|
||||
@@ -229,7 +250,9 @@ class _ParsedInjectField {
|
||||
|
||||
_ParsedInjectField({
|
||||
required this.fieldName,
|
||||
required this.parsedType,
|
||||
required this.coreType,
|
||||
required this.isFuture,
|
||||
required this.isNullable,
|
||||
this.scopeName,
|
||||
this.namedValue,
|
||||
});
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
// https://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:code_builder/code_builder.dart';
|
||||
|
||||
import 'type_parser.dart';
|
||||
|
||||
/// Small helpers for building code_builder AST nodes used by generators.
|
||||
class CodeBuilderEmitters {
|
||||
/// Builds a CherryPick scope opener expression.
|
||||
///
|
||||
/// - If [scopeName] is empty or null, uses openRootScope().
|
||||
/// - Otherwise uses openScope(scopeName: ...).
|
||||
static Expression openScope({String? scopeName}) {
|
||||
if (scopeName == null || scopeName.isEmpty) {
|
||||
return refer('CherryPick').property('openRootScope').call([]);
|
||||
}
|
||||
return refer(
|
||||
'CherryPick',
|
||||
).property('openScope').call([], {'scopeName': literalString(scopeName)});
|
||||
}
|
||||
|
||||
/// Builds a TypeReference appropriate for resolving a dependency.
|
||||
///
|
||||
/// For Future<T>, it returns the inner type reference (T).
|
||||
/// Nullability and generic arguments are preserved.
|
||||
static TypeReference resolveTypeRef(ParsedType parsedType) {
|
||||
final target = parsedType.isFuture && parsedType.innerType != null
|
||||
? parsedType.innerType!
|
||||
: parsedType;
|
||||
return _typeRefFromParsedType(target, stripNullability: true);
|
||||
}
|
||||
|
||||
/// Builds a DI resolve call on [scopeExpr] using [parsedType] and [named].
|
||||
///
|
||||
/// The method name is derived from [ParsedType.resolveMethodName].
|
||||
static Expression resolveCall({
|
||||
required Expression scopeExpr,
|
||||
required ParsedType parsedType,
|
||||
String? named,
|
||||
}) {
|
||||
final typeRef = resolveTypeRef(parsedType);
|
||||
final method = parsedType.resolveMethodName;
|
||||
final args = <Expression>[];
|
||||
final namedArgs = <String, Expression>{};
|
||||
if (named != null && named.isNotEmpty) {
|
||||
namedArgs['named'] = literalString(named);
|
||||
}
|
||||
return scopeExpr.property(method).call(args, namedArgs, [typeRef]);
|
||||
}
|
||||
|
||||
static TypeReference _typeRefFromParsedType(
|
||||
ParsedType parsedType, {
|
||||
required bool stripNullability,
|
||||
}) {
|
||||
return TypeReference((b) {
|
||||
b
|
||||
..symbol = parsedType.coreType
|
||||
..isNullable = stripNullability ? false : parsedType.isNullable;
|
||||
if (parsedType.typeArguments.isNotEmpty) {
|
||||
b.types.addAll(
|
||||
parsedType.typeArguments.map(
|
||||
(arg) =>
|
||||
_typeRefFromParsedType(arg, stripNullability: stripNullability),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,6 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
Element2 element,
|
||||
) {
|
||||
final buffer = StringBuffer();
|
||||
final location = _safeLocation(element);
|
||||
final enclosing = _safeEnclosingDisplayName(element);
|
||||
|
||||
// Header with category
|
||||
buffer.writeln('[$category] $message');
|
||||
@@ -76,11 +74,18 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
buffer.writeln('Context:');
|
||||
buffer.writeln(' Element: ${element.displayName}');
|
||||
buffer.writeln(' Type: ${element.runtimeType}');
|
||||
buffer.writeln(' Location: $location');
|
||||
buffer.writeln(
|
||||
' Location: ${element.firstFragment.libraryFragment?.source.fullName ?? 'unknown'}',
|
||||
);
|
||||
|
||||
// Try to show enclosing element info for extra context
|
||||
try {
|
||||
final enclosing = (element as dynamic).enclosingElement;
|
||||
if (enclosing != null) {
|
||||
buffer.writeln(' Enclosing: $enclosing');
|
||||
buffer.writeln(' Enclosing: ${enclosing.displayName}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if enclosingElement is not available
|
||||
}
|
||||
|
||||
// Arbitrary user context
|
||||
@@ -100,34 +105,6 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Best-effort extraction of element location for diagnostics.
|
||||
///
|
||||
/// Some tests use lightweight mocks for [Element2] that don't implement
|
||||
/// analyzer fragment APIs, so this method must never throw.
|
||||
static String _safeLocation(Element2 element) {
|
||||
try {
|
||||
return element.firstFragment.libraryFragment?.source.fullName ??
|
||||
'unknown';
|
||||
} catch (_) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort extraction of enclosing element display name.
|
||||
///
|
||||
/// Accessed via dynamic to stay compatible with analyzer API differences.
|
||||
static String? _safeEnclosingDisplayName(Element2 element) {
|
||||
try {
|
||||
final enclosing = (element as dynamic).enclosingElement;
|
||||
if (enclosing == null) return null;
|
||||
final name = enclosing.displayName;
|
||||
if (name is String && name.isNotEmpty) return name;
|
||||
return enclosing.toString();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,7 @@ name: cherrypick_generator
|
||||
description: |
|
||||
Source code generator for the cherrypick dependency injection system. Processes annotations to generate binding and module code for Dart & Flutter projects.
|
||||
|
||||
version: 4.0.0-dev.1
|
||||
version: 4.0.0-dev.0
|
||||
homepage: https://cherrypick-di.netlify.app
|
||||
documentation: https://cherrypick-di.netlify.app/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick/cherrypick_generator
|
||||
@@ -22,12 +22,9 @@ dependencies:
|
||||
cherrypick_annotations: ^4.0.0-dev.0
|
||||
analyzer: ">=8.2.9 <10.0.1"
|
||||
dart_style: ^3.0.0
|
||||
code_builder: ^4.10.1
|
||||
build: ^3.0.0
|
||||
source_gen: ^4.2.0
|
||||
collection: ^1.18.0
|
||||
package_config: ^2.2.0
|
||||
build_runner_core: ^9.3.1
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^6.0.0
|
||||
|
||||
32
cherrypick_generator/test/cherrypick_generator_test.dart
Normal file
32
cherrypick_generator/test/cherrypick_generator_test.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// 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
|
||||
// https://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:test/test.dart';
|
||||
|
||||
// Import working test suites
|
||||
import 'simple_test.dart' as simple_tests;
|
||||
import 'bind_spec_test.dart' as bind_spec_tests;
|
||||
import 'metadata_utils_test.dart' as metadata_utils_tests;
|
||||
// Import integration test suites (now working!)
|
||||
import 'module_generator_test.dart' as module_generator_tests;
|
||||
import 'inject_generator_test.dart' as inject_generator_tests;
|
||||
|
||||
void main() {
|
||||
group('CherryPick Generator Tests', () {
|
||||
group('Simple Tests', simple_tests.main);
|
||||
group('BindSpec Tests', bind_spec_tests.main);
|
||||
group('MetadataUtils Tests', metadata_utils_tests.main);
|
||||
group('ModuleGenerator Tests', module_generator_tests.main);
|
||||
group('InjectGenerator Tests', inject_generator_tests.main);
|
||||
});
|
||||
}
|
||||
@@ -11,13 +11,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:build/build.dart';
|
||||
import 'package:build_test/build_test.dart';
|
||||
import 'package:build_runner_core/build_runner_core.dart';
|
||||
import 'package:cherrypick_generator/inject_generator.dart';
|
||||
import 'package:package_config/package_config.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@@ -44,8 +40,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -79,8 +75,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -117,8 +113,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -128,8 +124,9 @@ part of 'test_widget.dart';
|
||||
|
||||
mixin _\$TestWidget {
|
||||
void _inject(TestWidget instance) {
|
||||
instance.service =
|
||||
CherryPick.openRootScope().resolve<MyService>(named: 'myService');
|
||||
instance.service = CherryPick.openRootScope().resolve<MyService>(
|
||||
named: 'myService',
|
||||
);
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -154,8 +151,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -165,8 +162,9 @@ part of 'test_widget.dart';
|
||||
|
||||
mixin _\$TestWidget {
|
||||
void _inject(TestWidget instance) {
|
||||
instance.service =
|
||||
CherryPick.openRootScope().tryResolve<MyService>(named: 'myService');
|
||||
instance.service = CherryPick.openRootScope().tryResolve<MyService>(
|
||||
named: 'myService',
|
||||
);
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -193,8 +191,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -231,8 +229,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -242,9 +240,9 @@ part of 'test_widget.dart';
|
||||
|
||||
mixin _\$TestWidget {
|
||||
void _inject(TestWidget instance) {
|
||||
instance.service =
|
||||
CherryPick.openScope(scopeName: 'userScope')
|
||||
.resolve<MyService>(named: 'myService');
|
||||
instance.service = CherryPick.openScope(
|
||||
scopeName: 'userScope',
|
||||
).resolve<MyService>(named: 'myService');
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -270,8 +268,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -305,8 +303,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -341,8 +339,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -352,8 +350,9 @@ part of 'test_widget.dart';
|
||||
|
||||
mixin _\$TestWidget {
|
||||
void _inject(TestWidget instance) {
|
||||
instance.service =
|
||||
CherryPick.openRootScope().resolveAsync<MyService>(named: 'myService');
|
||||
instance.service = CherryPick.openRootScope().resolveAsync<MyService>(
|
||||
named: 'myService',
|
||||
);
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -392,8 +391,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -404,11 +403,13 @@ part of 'test_widget.dart';
|
||||
mixin _\$TestWidget {
|
||||
void _inject(TestWidget instance) {
|
||||
instance.apiService = CherryPick.openRootScope().resolve<ApiService>();
|
||||
instance.cacheService =
|
||||
CherryPick.openRootScope().tryResolve<CacheService>(named: 'cache');
|
||||
instance.cacheService = CherryPick.openRootScope().tryResolve<CacheService>(
|
||||
named: 'cache',
|
||||
);
|
||||
instance.dbService =
|
||||
CherryPick.openScope(scopeName: 'dbScope')
|
||||
.resolveAsync<DatabaseService>();
|
||||
CherryPick.openScope(
|
||||
scopeName: 'dbScope',
|
||||
).resolveAsync<DatabaseService>();
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -438,8 +439,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -495,8 +496,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -532,8 +533,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -568,8 +569,8 @@ class TestWidget {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_widget.dart';
|
||||
|
||||
@@ -592,41 +593,10 @@ mixin _\$TestWidget {
|
||||
|
||||
/// Helper function to test code generation
|
||||
Future<void> _testGeneration(String input, String expectedOutput) async {
|
||||
final readerWriter = TestReaderWriter(rootPackage: 'a');
|
||||
await readerWriter.testing.loadIsolateSources();
|
||||
final packageConfig = await loadPackageConfigUri(
|
||||
(await Isolate.packageConfig)!,
|
||||
);
|
||||
final outputs = expectedOutput.isEmpty
|
||||
? null
|
||||
: {
|
||||
'a|lib/test_widget.inject.cherrypick.g.dart':
|
||||
decodedMatches(_normalizedEquals(expectedOutput)),
|
||||
};
|
||||
final result = await testBuilder(
|
||||
await testBuilder(
|
||||
injectBuilder(BuilderOptions.empty),
|
||||
{'a|lib/test_widget.dart': input},
|
||||
outputs: outputs,
|
||||
readerWriter: readerWriter,
|
||||
rootPackage: 'a',
|
||||
packageConfig: packageConfig,
|
||||
);
|
||||
if (expectedOutput.isEmpty && result.buildResult.status == BuildStatus.failure) {
|
||||
throw InvalidGenerationSourceError('Build failed');
|
||||
}
|
||||
}
|
||||
|
||||
Matcher _normalizedEquals(String expected) {
|
||||
return predicate<String>(
|
||||
(actual) => _normalize(actual) == _normalize(expected),
|
||||
'matches after normalization',
|
||||
outputs: {'a|lib/test_widget.inject.cherrypick.g.dart': expectedOutput},
|
||||
readerWriter: TestReaderWriter(),
|
||||
);
|
||||
}
|
||||
|
||||
String _normalize(String input) {
|
||||
return input
|
||||
.replaceAll(RegExp(r'\s+'), '')
|
||||
.replaceAll(RegExp(r',\)'), ')')
|
||||
.replaceAll(RegExp(r',\]'), ']')
|
||||
.replaceAll(RegExp(r',\}'), '}');
|
||||
}
|
||||
|
||||
@@ -11,16 +11,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:build/build.dart';
|
||||
import 'package:build_test/build_test.dart';
|
||||
import 'package:build_runner_core/build_runner_core.dart';
|
||||
import 'package:package_config/package_config.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:build_test/build_test.dart';
|
||||
import 'package:build/build.dart';
|
||||
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
import 'package:cherrypick_generator/module_generator.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
void main() {
|
||||
group('ModuleGenerator Tests', () {
|
||||
@@ -44,8 +40,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -79,8 +75,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -117,8 +113,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -153,8 +149,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -191,8 +187,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -228,8 +224,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -268,8 +264,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -303,8 +299,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -338,8 +334,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -378,8 +374,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -418,8 +414,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -457,8 +453,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -492,8 +488,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -545,8 +541,8 @@ abstract class TestModule extends Module {
|
||||
''';
|
||||
|
||||
const expectedOutput = '''
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'test_module.dart';
|
||||
|
||||
@@ -641,41 +637,10 @@ abstract class TestModule extends Module {
|
||||
|
||||
/// Helper function to test code generation
|
||||
Future<void> _testGeneration(String input, String expectedOutput) async {
|
||||
final readerWriter = TestReaderWriter(rootPackage: 'a');
|
||||
await readerWriter.testing.loadIsolateSources();
|
||||
final packageConfig = await loadPackageConfigUri(
|
||||
(await Isolate.packageConfig)!,
|
||||
);
|
||||
final outputs = expectedOutput.isEmpty
|
||||
? null
|
||||
: {
|
||||
'a|lib/test_module.module.cherrypick.g.dart':
|
||||
decodedMatches(_normalizedEquals(expectedOutput)),
|
||||
};
|
||||
final result = await testBuilder(
|
||||
await testBuilder(
|
||||
moduleBuilder(BuilderOptions.empty),
|
||||
{'a|lib/test_module.dart': input},
|
||||
outputs: outputs,
|
||||
readerWriter: readerWriter,
|
||||
rootPackage: 'a',
|
||||
packageConfig: packageConfig,
|
||||
);
|
||||
if (expectedOutput.isEmpty && result.buildResult.status == BuildStatus.failure) {
|
||||
throw InvalidGenerationSourceError('Build failed');
|
||||
}
|
||||
}
|
||||
|
||||
Matcher _normalizedEquals(String expected) {
|
||||
return predicate<String>(
|
||||
(actual) => _normalize(actual) == _normalize(expected),
|
||||
'matches after normalization',
|
||||
outputs: {'a|lib/test_module.module.cherrypick.g.dart': expectedOutput},
|
||||
readerWriter: TestReaderWriter(),
|
||||
);
|
||||
}
|
||||
|
||||
String _normalize(String input) {
|
||||
return input
|
||||
.replaceAll(RegExp(r'\s+'), '')
|
||||
.replaceAll(RegExp(r',\)'), ')')
|
||||
.replaceAll(RegExp(r',\]'), ']')
|
||||
.replaceAll(RegExp(r',\}'), '}');
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ packages:
|
||||
path: "../../cherrypick_annotations"
|
||||
relative: true
|
||||
source: path
|
||||
version: "4.0.0-dev.0"
|
||||
version: "3.0.2"
|
||||
cherrypick_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -148,7 +148,7 @@ packages:
|
||||
path: "../../cherrypick_generator"
|
||||
relative: true
|
||||
source: path
|
||||
version: "4.0.0-dev.0"
|
||||
version: "3.0.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -182,14 +182,14 @@ packages:
|
||||
path: "../../cherrypick_annotations"
|
||||
relative: true
|
||||
source: path
|
||||
version: "4.0.0-dev.0"
|
||||
version: "3.0.2"
|
||||
cherrypick_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
path: "../../cherrypick_generator"
|
||||
relative: true
|
||||
source: path
|
||||
version: "4.0.0-dev.0"
|
||||
version: "3.0.2"
|
||||
cli_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -12,7 +12,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
cherrypick: ^3.0.3
|
||||
cherrypick: any
|
||||
cherrypick_annotations: ^4.0.0-dev.0
|
||||
|
||||
dio: ^5.4.0
|
||||
@@ -27,7 +27,7 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
talker_flutter: ^5.0.0
|
||||
talker_cherrypick_logger: ^3.0.3
|
||||
talker_cherrypick_logger: any
|
||||
talker_dio_logger: ^5.0.0
|
||||
talker_bloc_logger: ^5.0.0
|
||||
|
||||
@@ -38,7 +38,7 @@ dev_dependencies:
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
build_runner: ^2.5.0
|
||||
cherrypick_generator: ^4.0.0-dev.1
|
||||
cherrypick_generator: ^4.0.0-dev.0
|
||||
|
||||
json_serializable: ^6.9.0
|
||||
retrofit_generator: ^10.0.5
|
||||
|
||||
@@ -60,10 +60,8 @@ scripts:
|
||||
scope: ["cherrypick_flutter"]
|
||||
|
||||
codegen:
|
||||
description: "Run build for Flutter packages only"
|
||||
exec: dart run build_runner build --delete-conflicting-outputs
|
||||
packageFilters:
|
||||
scope: ["postly"]
|
||||
run: |
|
||||
melos exec --scope="postly" -- dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
outdated:
|
||||
exec: dart pub outdated
|
||||
|
||||
@@ -64,10 +64,14 @@
|
||||
- **WHEN** метод помечен одновременно `@instance` и `@provide`
|
||||
- **THEN** генератор завершает сборку с ошибкой валидации
|
||||
|
||||
#### Scenario: Требования к @named
|
||||
- **WHEN** `@named` использует пустую строку или некорректный идентификатор
|
||||
#### Scenario: Требования к @named на provider-методе
|
||||
- **WHEN** `@named` на provider-методе использует пустую строку или некорректный идентификатор
|
||||
- **THEN** генератор завершает сборку с ошибкой валидации
|
||||
|
||||
#### Scenario: Пустой @named на inject-поле
|
||||
- **WHEN** `@named('')` указан на поле с `@inject`
|
||||
- **THEN** генератор трактует поле как безымянный резолв (без параметра `named`)
|
||||
|
||||
#### Scenario: Валидность @module
|
||||
- **WHEN** класс с `@module` не имеет публичных методов
|
||||
- **THEN** генератор завершает сборку с ошибкой валидации
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
- **THEN** subscope удаляется из дерева, а связанные ресурсы освобождаются
|
||||
|
||||
#### Scenario: Путь scope и разделитель
|
||||
- **WHEN** scope открывается по иерархическому пути с разделителем
|
||||
- **WHEN** вызывается `CherryPick.openScope(scopeName: ..., separator: ...)` с иерархическим путем
|
||||
- **THEN** создается цепочка subscopes по каждому сегменту пути
|
||||
|
||||
#### Scenario: Пустой scopeName
|
||||
- **WHEN** scope открывается с пустым именем
|
||||
- **WHEN** вызывается `CherryPick.openScope(scopeName: '')`
|
||||
- **THEN** возвращается root scope
|
||||
|
||||
### Requirement: Установка и удаление модулей
|
||||
@@ -81,8 +81,8 @@
|
||||
### Requirement: Ошибки несоответствия sync/async
|
||||
Резолв MUST выбрасывать ошибки при несоответствии sync/async режима.
|
||||
|
||||
#### Scenario: ResolveSync для async‑инстанса
|
||||
- **WHEN** binding зарегистрирован как async‑инстанс или async‑provider, а вызывается `resolveSync`
|
||||
#### Scenario: Синхронный резолв для async‑binding
|
||||
- **WHEN** binding зарегистрирован как async‑инстанс или async‑provider, а вызывается `resolve<T>()` или `tryResolve<T>()`
|
||||
- **THEN** выбрасывается ошибка с указанием использовать async‑резолв
|
||||
|
||||
### Requirement: Управление Disposable
|
||||
@@ -111,7 +111,7 @@
|
||||
- **THEN** observer получает соответствующие уведомления
|
||||
|
||||
### Requirement: Ошибки и сообщения об ошибках
|
||||
При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением, а для tryResolve MUST не бросать исключения.
|
||||
При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением. Для отсутствующей зависимости `tryResolve`/`tryResolveAsync` MUST возвращать `null` без исключения; ошибки выполнения резолва (например, цикл, sync/async mismatch, отсутствие обязательных params) MAY быть проброшены.
|
||||
|
||||
#### Scenario: Ошибка отсутствующей зависимости
|
||||
- **WHEN** вызывается `resolve<T>()` для незарегистрированной зависимости
|
||||
|
||||
@@ -41,14 +41,14 @@ Flutter‑интеграция MUST предоставлять `CherryPickProvid
|
||||
|
||||
#### Scenario: Ошибка lookup
|
||||
- **WHEN** вызов происходит вне поддерева провайдера
|
||||
- **THEN** происходит assertion‑ошибка
|
||||
- **THEN** в debug‑режиме происходит assertion‑ошибка
|
||||
|
||||
### Requirement: Ошибки и сообщения
|
||||
При отсутствии провайдера в дереве MUST быть диагностируемая ошибка.
|
||||
|
||||
#### Scenario: Диагностика отсутствия провайдера
|
||||
- **WHEN** `CherryPickProvider.of(context)` не находит провайдер
|
||||
- **THEN** сообщение об ошибке указывает на отсутствие провайдера
|
||||
- **THEN** в debug‑режиме сообщение assertion указывает на отсутствие провайдера
|
||||
|
||||
### Requirement: Точки расширения
|
||||
Flutter‑интеграция MUST позволять использовать собственные DI‑scope стратегии поверх `CherryPickProvider`.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
## 3.0.3
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 3.0.2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: talker_cherrypick_logger
|
||||
description: A Talker logger integration for CherryPick DI to observe and log DI events and errors.
|
||||
version: 3.0.3
|
||||
version: 3.0.2
|
||||
homepage: https://cherrypick-di.netlify.app
|
||||
documentation: https://cherrypick-di.netlify.app/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick
|
||||
@@ -18,7 +18,7 @@ environment:
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
talker: ^5.0.0
|
||||
cherrypick: ^3.0.3
|
||||
cherrypick: ^3.0.2
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user