Skip to content

fix: find async state machines in optimized builds - #500

Open
iamAdarshh wants to merge 1 commit into
TNG:mainfrom
iamAdarshh:fix-async-state-machine-lookup
Open

iamAdarshh wants to merge 1 commit into
TNG:mainfrom
iamAdarshh:fix-async-state-machine-lookup

Conversation

@iamAdarshh

Copy link
Copy Markdown

Fixes #498

Problem

HandleAsync found an async method's state machine by taking the first newobj in the method whose type has a MoveNext method. In optimized (Release) builds the compiler emits async state machines as structs, which are initialized in place instead of created with newobj. Nothing matched, so the fallback analysed the small outer method instead of MoveNext, and every dependency inside the method body was lost. For example, a NotDependOnAny rule on a class whose async method uses the forbidden type passed.

HandleIterator used the same lookup. It only worked because iterator state machines are always classes.

Fix

  • Add GetStateMachineType() next to IsAsync()/IsIterator() in MonoCecilMemberExtensions. It reads the state machine type from the method's AsyncStateMachineAttribute or IteratorStateMachineAttribute and resolves it.
  • Use it in HandleAsync and HandleIterator instead of the newobj lookup. The existing fallback still applies when the type can't be resolved or has no MoveNext.

Tests

None of the test assemblies set <Optimize>, so they were always built with class state machines. That's why the existing tests didn't catch this.

  • New TestAssemblies/OptimizedAssembly with <Optimize>true</Optimize>, containing an async method and an iterator method that call another type.
  • OptimizedStateMachineDependenciesTests checks that the dependency is found and that a NotDependOnAny rule fails, for both methods. A guard test asserts that the async state machine really is a struct, so the test can't silently stop covering this path if <Optimize> is ever removed.
  • Checked against the unfixed loader: the async test fails there and passes with the fix.
  • Removed the Release skip on MethodCallDependenciesAreFoundInAsyncMethod. It was marked [SkipInReleaseBuildTheory] in e75fe2b because of this bug. It fails in Release without the fix and passes with it. SkipInReleaseBuildTheory had no other users, so I removed it.

dotnet build, dotnet test -c Debug and mise run check pass.

Related gaps (not in this PR)

  • The branch for compiler-generated methods in CreateMethodBodyDependenciesRecursive handles iterators but never calls HandleAsync, so async lambdas and local functions are still missed. This looks like Analyse dependencies of async lambdas #230.
  • Methods returning IAsyncEnumerable carry AsyncIteratorStateMachineAttribute, which IsAsync() doesn't check for.
  • Unrelated to this change: MethodMemberSyntaxElementsTests.HaveDependencyInMethodBodyToTest and NotHaveDependencyInMethodBodyToTest fail on main when the tests run with -c Release. CI runs its tests in Debug, so it doesn't see them.

HandleAsync located the state machine by taking the first newobj in the
async method whose type has a MoveNext method. In optimized builds the
compiler emits async state machines as structs, which are initialized in
place instead of created with newobj. Nothing matched, the fallback
analysed the small outer method instead of MoveNext, and every
dependency in the method body was lost. HandleIterator used the same
lookup; it only worked because iterator state machines are always
classes.

Read the state machine type from the method's AsyncStateMachineAttribute
or IteratorStateMachineAttribute instead, and keep the existing fallback
for when it cannot be resolved.

None of the test assemblies set <Optimize>, so they always had class
state machines. Add OptimizedAssembly, which is always optimized, with an
async and an iterator method, and a guard test that its async state
machine really is a struct. MethodCallDependenciesAreFoundInAsyncMethod
was skipped in Release builds because of this bug; it now passes there,
so run it again and drop the SkipInReleaseBuildTheory attribute that
only it used.

Fixes TNG#498

Signed-off-by: Adarsh Choudhary <adarshchoudhary087@gmail.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.23077% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.25%. Comparing base (dcf76a3) to head (b963526).

Files with missing lines Patch % Lines
ArchUnitNET/Loader/MonoCecilMemberExtensions.cs 71.42% 2 Missing and 2 partials ⚠️
ArchUnitNET/Loader/TypeProcessor.cs 66.66% 0 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #500      +/-   ##
==========================================
- Coverage   86.33%   86.25%   -0.09%     
==========================================
  Files         260      260              
  Lines       12496    12498       +2     
  Branches     1216     1219       +3     
==========================================
- Hits        10789    10780       -9     
- Misses       1372     1379       +7     
- Partials      335      339       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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.

[Bug]: In Release builds every dependency inside an async method is lost, so negative rules pass vacuously (state machine found via first newobj)

2 participants