Skip to content

feat(mocks): TM010 analyzer + code fix for Mock.Of<T>() -> T.Mock() - #7307

Merged
thomhurst merged 2 commits into
mainfrom
feat/mocks-of-to-static-mock
Oct 31, 2026
Merged

thomhurst merged 2 commits into
mainfrom
feat/mocks-of-to-static-mock

Conversation

@thomhurst

@thomhurst thomhurst commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds an analyzer and code fix to TUnit.Mocks that move single-type mock creation from Mock.Of<T>() to the recommended T.Mock() syntax.

  • TM010 (Info, TUnit.Mocks.Analyzers): reports Mock.Of<T>() and Mock.Of<T>(behavior) calls that have a T.Mock() / T.Mock(behavior) equivalent.
  • New TUnit.Mocks.Analyzers.CodeFixers project: rewrites the call, keeps the type as written (qualified, generic or alias), keeps the behavior argument, drops a ? annotation, and adds using TUnit.Mocks; when the extension members aren't in scope. Fix All is supported.
  • Bundled into the TUnit.Mocks package at analyzers/dotnet/cs, next to the existing analyzer DLL. Verified with dotnet pack.
  • Adds DiagnosticIds consts. The fixer uses them, not Rules, following [Bug]: MissingFieldException for Latest migration analyser in VS2026 #6157, and a RulesDecouplingVerifier IL test enforces this.
  • Docs: added a TM010 paragraph to writing-tests/mocking/index.md, including the dotnet format command for migrating a whole project.

The ID is TM010 because the source generator already reports TM008 and TM009. A comment in DiagnosticIds records this.

When TM010 is not reported

The analyzer reports only where the fixed code still compiles. It follows the source generator's own rules for emitting T.Mock():

  • Language version below C# 14.
  • MockRepository.Of<T>(), multi-interface Of<T1, T2>(), the IDefaultValueProvider overload, and overloads that take constructor arguments.
  • The type is a type parameter or open generic, sealed, static, a delegate, a value type, file-local, or not accessible (e.g. a private nested type).
  • The type, a base type or an interface already has a member named Mock.
  • Interfaces with a ref-struct property or a static abstract member (no wrapper type, so no T.Mock()).
  • Interfaces where the result's type is inferred. For interfaces, T.Mock() returns the generated wrapper type, not Mock<T>. This covers a reassigned var local (top-level statements included), implicitly typed arrays, lambda returns, tuples and generic type inference.
  • The type name, used as an expression, binds to something else at the call site (e.g. a property with the same name as the type).
  • Adding using TUnit.Mocks; would make an existing imported name ambiguous (e.g. another library's Arg).

Known limitation: if an overloaded method takes both M(IFoo) and M(Mock<IFoo>), passing the interface wrapper can make the call ambiguous. This is rare and not detected.

Test plan

  • tests/TUnit.Mocks.Analyzers.Tests: 83/83 pass on net10.0. The project now pins Roslyn 4.14, so the code fix tests compile the fixed code against real C# 14 extension blocks that stand in for the generated ones.
  • End to end with the real source generator: a scratch project using Mock.Of<T>() for an interface, a closed generic interface and an abstract class, with fully qualified names and no using. dotnet format analyzers --diagnostics TM010 --severity info rewrote all 4 calls and added the using, and the result builds.
  • tests/TUnit.Mocks.Tests builds with the new analyzer, with no AD0001 crashes.
  • dotnet pack src/TUnit.Mocks contains analyzers/dotnet/cs/TUnit.Mocks.Analyzers.CodeFixers.dll.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an analyzer recommendation to use T.Mock() instead of supported Mock.Of<T>() calls in eligible C# 14+ projects.
    • Added a code fix that rewrites eligible calls, preserves behavior arguments and comments, and adds the required namespace import when needed.
    • Included the analyzer and code fix in the mocks package, with support for applying fixes individually or across a project.
  • Documentation
    • Documented the recommendation and instructions for applying fixes individually or across a project.

…ck()

Adds a TUnit.Mocks.Analyzers.CodeFixers project bundled into the TUnit.Mocks
package. TM010 (Info) reports Mock.Of<T>() and Mock.Of<T>(behavior) calls that
can use the generated T.Mock() / T.Mock(behavior) extension, and the code fix
rewrites them, adding `using TUnit.Mocks;` when needed.

Co-Authored-By: Claude <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T12:29:50.169580Z f3283da PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3a9a9959-ead4-47ae-bab0-dc57beed42cc
Commits

Reviewing files that changed from the base of the PR and between f3283da and 90c94f8.

Files selected for processing: 5
  • TUnit.CI.slnx
  • TUnit.Dev.slnx
  • src/TUnit.Mocks.Analyzers.CodeFixers/PreferStaticMockSyntaxCodeFixProvider.cs
  • src/TUnit.Mocks.Analyzers/PreferStaticMockSyntaxAnalyzer.cs
  • tests/TUnit.Mocks.Analyzers.Tests/PreferStaticMockSyntaxAnalyzerTests.cs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 4 remain after this review.

Walkthrough

The pull request adds TM010, which recommends T.Mock() for eligible Mock.Of<T>() calls. It adds a code fix, integrates the code-fix assembly into the package, documents the recommendation, and adds analyzer and code-fix tests.

Changes: Static Mock Syntax

Layer / File(s) Summary
Diagnostic and eligibility analysis
src/TUnit.Mocks.Analyzers/DiagnosticIds.cs, src/TUnit.Mocks.Analyzers/Rules.cs, src/TUnit.Mocks.Analyzers/PreferStaticMockSyntaxAnalyzer.cs, src/TUnit.Mocks.SourceGenerator/Diagnostics.cs
Adds diagnostic ID constants and the TM010 descriptor. The analyzer reports eligible calls for C# 14 or later and excludes unsupported calls, types, and contexts.
Code fix and package integration
src/TUnit.Mocks.Analyzers.CodeFixers/*, src/TUnit.Mocks/TUnit.Mocks.csproj, TUnit.slnx, TUnit.CI.slnx, TUnit.Dev.slnx, docs/docs/writing-tests/mocking/index.md
Adds a fix that rewrites supported calls to T.Mock() and adds using TUnit.Mocks; when needed. Adds the code-fix project to the solutions and package, and documents the diagnostic and fix-all options.
Analyzer and code-fix validation
tests/TUnit.Mocks.Analyzers.Tests/*
Adds tests for supported rewrites, excluded cases, and rules decoupling. Adds test helpers for code-fix verification and line-ending normalization.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant PreferStaticMockSyntaxAnalyzer
  participant PreferStaticMockSyntaxCodeFixProvider
  participant RoslynDocument
  Developer->>PreferStaticMockSyntaxAnalyzer: Provide source with Mock.Of<T>()
  PreferStaticMockSyntaxAnalyzer->>Developer: Report TM010 for an eligible call
  Developer->>PreferStaticMockSyntaxCodeFixProvider: Apply the code fix
  PreferStaticMockSyntaxCodeFixProvider->>RoslynDocument: Replace call with T.Mock() and add using when needed
  RoslynDocument->>Developer: Return updated source
Loading

Suggested reviewers: quattordici

Merge Risk: Minimal · up to 90c94

No identified issue currently blocks merging, subject to normal build and test checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 39.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely identifies the TM010 analyzer and code fix that replaces supported Mock.Of() calls with T.Mock().
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 39.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 28 functions across 9 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A rabbit reads the syntax bright,
And hops from Of to Mock outright.
A using joins when out of scope,
While comments stay and tests give hope.
Fix-all runs; the lines align,
The rabbit nibbles, pleased to find.

@github-actions

Copy link
Copy Markdown
Contributor

Review: TM010 prefer T.Mock() analyzer and code fix

I read the analyzer and the code fixer. I didn't build the project or run the tests. I found nothing that should block the merge.

Strengths

  • The analyzer has to bind symbols to decide whether to report, so it runs the cheap syntactic checks first (Of name, single type argument, at most one argument). That keeps the hot path cheap.
  • It deliberately skips cases where the fix would not compile or would change meaning: C# below 14, a Mock member already on the type, types the generator doesn't emit T.Mock() for, and var/inference positions where the wrapper type would leak. A fixer that never produces broken code is worth the extra analyzer logic.
  • The fixer uses DiagnosticIds rather than Rules, following [Bug]: MissingFieldException for Latest migration analyser in VS2026 #6157, and CodeFixerRulesDecouplingTests enforces that.
  • Fix All works per document. It batches the replacements with ReplaceNodes and adds the using once.
  • IsMocksNamespaceInScope handles the enclosing namespace, and it correctly ignores a using Mock = ... alias, which doesn't import extension members.

Suggestions (non-blocking)

  1. Duplicated generator rules. IsStaticMockGeneratedFor copies the source generator's conditions for emitting T.Mock(). If the generator changes, the analyzer will drift silently. Moving the predicate into a shared internal file linked into both projects would give one source of truth. At minimum, add a test that checks a type the generator skips is also skipped by TM010.
  2. Fixer-side CreateReplacement check. It is called once to decide whether to fix and again in ReplaceNodes, so each invocation is built twice. Caching the result in a dictionary keyed by node would remove the duplicate work and the ?? rewritten fallback. This is minor.
  3. Generic receivers. ParseExpression on typeArgument.ToFullString() is fine for names. For generic arguments such as IFoo<int> it parses as an expression, and a nested type argument that is itself nullable (IFoo<string?>) can be misparsed. Please check that a test covers this, or have the analyzer skip it.
  4. Docs. The dotnet format migration tip is helpful. Consider noting that TM010 is Info severity, so it needs --severity info to apply.

Overall this is well-scoped and carefully tested. Approving in spirit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3283da975

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the remaining comment-loss case is non-blocking.

Summary

Adds TM010 and a code fix for supported Mock.Of<T>() calls.

  • Supported mock factory calls get a suggestion to use T.Mock().
  • The code fix rewrites supported calls and keeps their mock settings.

Reviews (2) · Last reviewed commit: "fix(mocks): address TM010 review finding..." · Reviewed by Greptile

- Add TUnit.Mocks.Analyzers.CodeFixers to TUnit.CI.slnx and TUnit.Dev.slnx so
  the CI library build produces its Release output before `pack --no-build`.
- Interface `var` locals: report only when every use of the local is
  type-insensitive (member access, non-generic argument, explicitly typed
  target, declared return). Writes, tuple deconstruction and inferred uses such
  as Task.FromResult(mock) now suppress the diagnostic.
- Check every simple name in the file, not just the call site, before relying
  on adding a file-level `using TUnit.Mocks;`.
- Keep comments from the removed `Mock.Of<...>` tokens.
- Test stubs return a wrapper subclass for interfaces, matching the generator.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Review of #7307 (TM010 prefer T.Mock())

I read the analyzer diff and did not build or run the tests.

What I like

  • The analyzer only reports where the generator emits T.Mock(): the C# 14 check, the sealed/static/file-local/inaccessible checks, a member named Mock, ref-struct and static-abstract interfaces. So the fix should not produce code that fails to compile. The syntactic pre-filter runs before any semantic binding, which keeps the cost low.
  • The inferred-type guard for interfaces is the hardest part, and it is handled conservatively: an unrecognised position counts as type-sensitive, so the analyzer stays silent.
  • The fixer uses DiagnosticIds instead of Rules, with an IL test enforcing that. The new project is bundled into the package, and the docs are updated.

Suggestions (non-blocking)

  1. Duplicated generator rules. IsStaticMockGeneratedFor copies the source generator's conditions by hand, so the two can drift. A change to the generator, such as supporting more types, would leave TM010 silently under-reporting, or over-reporting with a fix that doesn't compile. Two options:
    • Share the predicate through a common source-linked file used by both projects.
    • Add a test that runs the generator and the analyzer over the same set of types and asserts they agree.
  2. Complexity of the MayChangeInferredType allowlist. It is long and has many branches, and it is the most likely place for a missed case. Please make sure the analyzer tests cover each branch, in particular var reassignment, lambda returns, generic inference and tuple deconstruction. Also consider a cheaper default: report only when the target is explicitly typed or the call is a statement-level var declaration whose uses are never reassigned. That would drop a lot of code.
  3. Hot path cost. ImportWouldCauseAmbiguity and invocation.SyntaxTree.GetRoot() run per candidate invocation. Cache the result per syntax tree in the compilation-start action, or compute it lazily, so large files with many Mock.Of calls don't repeat the work.
  4. Fixer edge cases. Check that the fix keeps trivia and comments, and handles a conditional-access chain such as Mock.Of<T>()?.X. Fix All with the using insertion needs a test with multiple diagnostics in one file, if there isn't one already.

Overall this is solid, careful work. I found no blocking issues.

@thomhurst

Copy link
Copy Markdown
Owner Author

Thanks for the review. Here is what I did with each suggestion, as of 90c94f8:

  1. Duplicated generator rules. I kept the predicate in the analyzer. The two projects ship as separate assemblies, and the generator decides per variant, so a shared linked file would still need both sides to stay in sync. A drift test is the more useful guard, and I'm happy to add one as a follow-up. The interface test stubs now return a wrapper subclass like the generator does, so a fix that changes a type the generator emits fails to compile in the tests.
  2. CreateReplacement runs twice. I left this as is. A Mock.Of<T>() call can't contain another fixable call, so the second run is cheap and keeps the ReplaceNodes callback self-contained.
  3. Nullable nested type arguments. Covered. Nullable_Nested_Type_Argument_Is_Rewritten checks that Mock.Of<IGeneric<string?>>() becomes IGeneric<string?>.Mock(). The analyzer also binds the reparsed receiver speculatively, so anything that misparses is not reported at all.
  4. Docs. The docs already pass --severity info in the dotnet format command.

Also in that commit:

  • The CI pack --no-build failure is fixed. I added the CodeFixers project to TUnit.CI.slnx and TUnit.Dev.slnx.
  • The var-local, file-wide using, tuple deconstruction and comment findings from the other reviews are fixed.

This branch was previously deployed

1 inactive deployment
Pull Requests — 90c94f86 Deployed Oct 31, 2026 by thomhurst via modularpipeline (ubuntu-latest) #20470
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.

1 participant