Skip to content

MA0042: check if DisposeAsync is declared directly when runtime type is known#1118

Merged
meziantou merged 4 commits into
mainfrom
meziantou/ma0042-disposeasync-override-check
May 4, 2026
Merged

MA0042: check if DisposeAsync is declared directly when runtime type is known#1118
meziantou merged 4 commits into
mainfrom
meziantou/ma0042-disposeasync-override-check

Conversation

@meziantou
Copy link
Copy Markdown
Owner

@meziantou meziantou commented May 4, 2026

Contributes to #1117

Problem

MA0042 was reporting false positives for types like SqlConnection or MemoryStream that inherit DisposeAsync from a base class but don't actually override it. For example, DbConnection.DisposeAsync is just a virtual method that calls Dispose() synchronously — using await using brings no benefit when the runtime type is known to not override it.

Solution

The fix distinguishes two cases based on whether the static type information tells us the exact runtime type:

Situation Diagnostic? Reason
new SqlConnection() ❌ No Exact type known; SqlConnection doesn't declare DisposeAsync
CreateConnection()SqlConnection ✅ Yes Runtime type could be a subclass with a real override
new MyConn() where MyConn overrides DisposeAsync ✅ Yes Exact type known; it has a direct declaration
sealed SealedConn without override, from factory ❌ No Sealed = exact type known; no direct declaration
sealed SealedConn with override, from factory ✅ Yes Sealed = exact type known; has direct declaration
new MemoryStream() ❌ No MemoryStream doesn't declare DisposeAsync (replaces the hardcoded special case)

Key changes

  • DoNotUseBlockingCallInAsyncContextAnalyzer.cs: Added HasDisposeAsyncMethodDeclaredDirectly() which uses GetMembers() (direct only) instead of GetAllMembers(). Modified CanBeAwaitUsing() to use it when the runtime type is statically known (direct object creation or sealed type). Removed the now-redundant MemoryStream special case and its field.
  • Test file: Added 5 new tests covering all the new behaviors.

meziantou and others added 4 commits May 4, 2026 15:08
…is known

When analyzing 'using' statements and declarations:
- For direct object creation (new T()) or sealed types, only report
  a diagnostic if the type itself declares or overrides DisposeAsync.
  Inheriting a non-async default DisposeAsync (e.g. DbConnection's
  implementation that just calls Dispose()) is not a meaningful override,
  so 'await using' would bring no benefit.
- For all other expressions (factory methods, non-sealed types), keep
  the existing full-hierarchy check since the runtime type may be a
  subclass that properly overrides DisposeAsync.

Also removes the now-redundant MemoryStream special case; the general
IObjectCreationOperation check already handles it correctly because
MemoryStream does not declare DisposeAsync directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
For DbConnection subclasses, only warn about 'using' (instead of
'await using') when the exact type being instantiated has a meaningful
DisposeAsync override. DbConnection.DisposeAsync itself just calls
Dispose() synchronously, so inheriting it without overriding brings
no benefit from 'await using'.

- Restore MemoryStream special case (unchanged)
- Add DbConnectionSymbol field for System.Data.Common.DbConnection
- Add HasDisposeAsyncMethodDeclaredInDbConnectionSubclass: walks the
  type hierarchy from T up to (but not including) DbConnection and
  looks for a DisposeAsync declaration via GetMembers()
- In CanBeAwaitUsing: for 'new T()' where T : DbConnection, use the
  new method; factory/method calls retain the old conservative
  behavior (warn, since the runtime type may be a deeper subclass)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of a special-cased MemoryStream exemption, apply the same
DisposeAsync-override walk used for DbConnection to all Stream subclasses:
when a Stream subclass is directly instantiated with 'new' and does not
override DisposeAsync in the subclass chain (up to but not including
Stream), do not report a diagnostic.

- Replace MemoryStreamSymbol with StreamSymbol (System.IO.Stream)
- Replace HasDisposeAsyncMethodDeclaredInDbConnectionSubclass with the
  generic HasDisposeAsyncMethodDeclaredInSubclass(symbol, baseTypeSymbol)
- CanBeAwaitUsing now applies the check for both Stream and DbConnection
- Add two new Stream-specific tests
- Update MA0042.md to document Stream alongside DbConnection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@meziantou meziantou merged commit fa3e705 into main May 4, 2026
13 checks passed
@meziantou meziantou deleted the meziantou/ma0042-disposeasync-override-check branch May 4, 2026 23:41
This was referenced May 5, 2026
This was referenced May 11, 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