Visitar URL original
fix(hottesting): Create takes the services a view model reads while it is built by rajamatt · Pull Request #3285 · unoplatform/uno.extensions · GitHub
Skip to content

fix(hottesting): Create takes the services a view model reads while it is built - #3285

Open
rajamatt wants to merge 6 commits into
mainfrom
dev/mara/mock-create-eager-services
Open

rajamatt wants to merge 6 commits into
mainfrom
dev/mara/mock-create-eager-services

Conversation

@rajamatt

@rajamatt rajamatt commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

GitHub Issue (If applicable): closes #3282

PR Type

  • Bugfix

What is the current behavior?

{Vm}Mock.Create null-injects every constructor parameter except a messenger. A model that reads a service while its view model is built throws NullReferenceException in Create, before any mock is applied:

  • a feed written as a method group, ListFeed.Async(Service.GetItems): the generated view-model constructor builds every feed, and binding the method group reads Service;
  • a constructor that dereferences a service.

What is the new behavior?

  • Create takes such a parameter from its caller: Create(service) and Create(service, mock). This is spec 013's R1, which was planned but not built.
  • The analysis counts a parameter read outside a lambda in a feed body as eager, as it already did in the constructor body:
    • this includes a feed set in a property initializer;
    • a receiver that is the member a parameter feeds (a positional record's property, a field assigned in the constructor, an expression-bodied one included) counts as that parameter;
    • this., parentheses, ! and casts are seen through.
  • The mocking generator reads [CtorDependency(Eager = true)], hand-declared ones included, and on the source path also runs the analysis.
  • A model with no such parameter keeps Create() and Create(mock), emitted exactly as before.
  • A messenger keeps its stopgap and still gets a fresh WeakReferenceMessenger.

Reviewer's guide

  • Start here: FeedDependencyAnalysis.FindEagerCtorParameters and DereferencedName, then FeedsMockGenerator.Generate and IsTakenFromCaller.
  • MVUX generator: one line. Through the shared analysis it now also emits [CtorDependency(Eager = true)] for a parameter a feed body reads eagerly.
  • Tests that prove it: the first commit, 62e716da0, adds tests only.
    • Given_GeneratedMock.When_FeedIsAMethodGroup_Then_CreateTakesTheServiceAndInputFlows (fixture ShelfModel): on main the test project does not compile, because Create(service, mock) does not exist.
    • Given_FeedsMockGenerator: a method-group feed, an eager [CtorDependency], and parameters named mock, vm and event fail on main. A lambda feed, whose service stays null-injected, passes on both.
    • When_ModelCarriesNoMetadata_Then_MockIsGeneratedFromSource expects a different call: its feed, Service.Items, reads the service while the view model is built.
  • Review fixes: 424599cd2, with its tests. These are the other spellings (this., !, a cast, a property initializer), a field assigned in an expression-bodied constructor, and a hand-declared attribute on the source path.
  • Docs commits: 963277482 and 4f860ed59: testing.md, rules.md and spec 013 (spec §8, architecture, implementation, history v20).
  • Unrelated, one line: 40e7e12aa replaces a hard tab in spec 026 that came from main (fix(reactive): MVUX hot reload of a command's parameters crashes or loses list updates #3296), so the docs validation passes.

PR Checklist

  • Tested code with current supported SDKs: unit tests only (Uno.HotTesting.Reactive.Tests 51 pass; Uno.Extensions.Reactive.Tests 1654 pass, 19 skipped; Uno.Extensions.Core.Tests 61 pass). UI and runtime tests were not run locally.
  • Docs have been added/updated.
  • Unit Tests for the changes have been added: 8 new (11 cases), 1 updated.
  • Contains NO breaking changes: see below.
  • Associated with an issue.

Breaking change: a model whose view model reads a service while it is built loses Create() and Create(mock), which threw anyway, and gets Create(service) and Create(service, mock) instead. Callers pass a service, often a fake. The mocked feeds never call it. Because the change is breaking, it should not be backported to release/stable/8.0.

Other information

🤖 Generated with Claude Code

@rajamatt rajamatt self-assigned this Nov 3, 2026
{
while (taken.Contains(name))
{
name = "_" + name;
Comment on lines +479 to +487
foreach (var attr in model.GetAttributes().Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, ctorDep)))
{
if (attr.ConstructorArguments is { Length: 1 } args
&& args[0].Value is string parameter
&& attr.NamedArguments.Any(n => n.Key == "Eager" && n.Value.Value is true))
{
yield return parameter;
}
}
rajamatt and others added 5 commits November 13, 2026 10:31
…n reads

- Fixture ShelfModel builds a feed from a method group (Service.GetBooks)
- Generator cases: a method-group feed, an eager [CtorDependency], parameters named like Create's own
- A lambda feed keeps its null-injected service
- Fails on main: Create(service, mock) does not exist and the method-group feed throws in Create

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n reads

- The analysis counts a parameter read outside a lambda in a feed's body as eager: the view-model's constructor builds every feed, so a method group (ListFeed.Async(Service.GetItems)) reads its service then
- A receiver that is the member a parameter feeds (a positional record's property, an assigned field) counts as that parameter
- Create reads [CtorDependency(Eager = true)] on the metadata path and runs the analysis on the source path, and takes those parameters from its caller instead of null-injecting them
- Create's own parameter and local are renamed when a forwarded parameter already uses the name; keyword names are escaped
- The messenger keeps its stopgap

Closes #3282

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion reads

- testing.md and rules.md: say which parameters Create takes as arguments; the RecipeModel example uses a lambda, so Create(mock) holds
- Spec 013: R1 built (architecture §2.1 and §2.2, implementation §3), history v20

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… more spellings

- A receiver is seen through this., parentheses, ! and casts (this.Service.GetItems)
- A feed set in a property initializer is analysed like one written as a getter
- A field assigned in an expression-bodied constructor (=> _service = service) maps to its parameter
- The source path also reads a hand-declared [CtorDependency(Eager = true)], the author's escape hatch
- An Eager argument that is not a plain value is ignored instead of throwing
- Create's parameter lists are built one way; CtorDependencyAttribute.Eager documents feed bodies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- spec §8: Create takes the mock record plus any service construction dereferences
- architecture §2.1: what the eager scan walks, and what a hand-declared attribute covers
- history v20: the extra spellings, why no diagnostic is needed, the metadata path needs a rebuild
- testing.md: the example lambda without async/await

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rajamatt
rajamatt force-pushed the dev/mara/mock-create-eager-services branch from 39b8f95 to 4f860ed Compare November 13, 2026 15:35
- Line 33 of specs/026-mvux-hr-replaced-model-ctor-guard/spec.md starts with a tab, which fails markdownlint MD010
- The docs validation runs on pull requests that change doc/, so this fails them until main is fixed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mergify

mergify Bot commented Nov 13, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue). If it does not match the queue conditions yet, it enters the queue once it does.

  • Queue this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mocking a view model throws when a feed is written as a method group

1 participant