Skip to content

Do not report MA0173 when the value cannot be captured by a lambda - #1487

Merged
meziantou merged 2 commits into
mainfrom
feature/ma0173-ref-lambda-capture-027ad4
Sep 12, 2026
Merged

meziantou merged 2 commits into
mainfrom
feature/ma0173-ref-lambda-capture-027ad4

Conversation

@meziantou

Copy link
Copy Markdown
Owner

Problem

The MA0173 code fix rewrites Interlocked.CompareExchange(ref field, value, null) into LazyInitializer.EnsureInitialized(ref field, () => value). It wraps the value expression in a lambda without checking that the expression can legally live inside one, so it can turn compiling code into code that does not compile:

using System.Threading;
public class Sample
{
    static object target;
    public static void Run(ref int value)
    {
        Interlocked.CompareExchange(ref target, new System.Text.StringBuilder(value), null);
    }
}

The fixed code fails with CS1628, as a lambda cannot capture the ref parameter value.

Change

UseLazyInitializerEnsureInitializeAnalyzer now walks the value operation before reporting and skips the diagnostic when any descendant is something a lambda cannot capture:

Operation Compiler error avoided
ref/out/in/ref readonly parameter reference CS1628
ref local reference CS8175
local or parameter of a ref struct type CS8175
this of a struct CS1673

Notes for the reviewer

  • The check is in the analyzer rather than the code fix: the point of the rule is "you can write this as EnsureInitialized", so when the lambda form cannot exist there is nothing to report. The fixer is only reachable through this diagnostic, so it cannot produce the broken code any more, and no unfixable diagnostic is left behind.
  • The ref struct and struct-this cases were not in the original report, but they are the same defect (a value that is legal as an argument and illegal inside a lambda), so they are handled together.
  • The check is deliberately conservative: it scans all the descendants, including nested lambdas, so new Foo((ref int x) => ...) is skipped even though hoisting it would compile. That is a missed diagnostic, not a broken fix.
  • docs/Rules/MA0173.md documents the exclusion.

Tests

Added to UseLazyInitializerEnsureInitializeAnalyzerTests: six no-diagnostic tests (RefParameter, OutParameter, InParameter, RefLocal, RefStructLocal, StructThis) and a Parameter test that still reports and fixes, to guard against over-restricting the rule.

All six new negative tests were confirmed to fail against the unmodified analyzer and to pass with the change.

Validation

  • dotnet build succeeds with no warnings.
  • MA0173 tests pass on all five Roslyn versions (4.8, 4.14, 5.0, 5.6, 5.9): 14 passed, 0 failed each.
  • Full suite on Roslyn 5.9: 4387 passed, 0 failed, 0 skipped. Full suite on Roslyn 4.8: 4270 passed, 0 failed, 0 skipped.
  • dotnet run --project src/DocumentationGenerator exits 0 with no markdown changes, re-run after the documentation edit.

The code fix moves the CompareExchange value into a lambda passed to
LazyInitializer.EnsureInitialized, which does not compile when that value
uses something a lambda cannot capture: a ref/out/in parameter (CS1628),
a ref local or a variable of a ref struct type (CS8175), or the "this"
reference of a struct (CS1673).

The analyzer now walks the value operation and skips the diagnostic in
those cases. The check is intentionally conservative: it scans all the
descendants, including nested lambdas, so a few reportable cases are
skipped instead of producing code that does not compile.
main added the MA0173 return-value exclusion (#1479), which touched the
same regions of the analyzer and of the rule documentation. Both
exclusions are kept: the diagnostic is skipped when the return value of
CompareExchange is used and when the value cannot be captured by a lambda.
@meziantou
meziantou enabled auto-merge (squash) September 12, 2026 03:38
@meziantou
meziantou merged commit 7337761 into main Sep 12, 2026
13 checks passed
@meziantou
meziantou deleted the feature/ma0173-ref-lambda-capture-027ad4 branch September 12, 2026 03:38
This was referenced Sep 12, 2026
This was referenced Sep 17, 2026
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