Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ Add the following nuget package to you project: https://www.nuget.org/packages/E
| [ERP041](https://github.com/SergeyTeplyakov/ErrorProne.NET/tree/master/docs/Rules/ERP041.md) | EventSource class should be sealed |
| [ERP042](https://github.com/SergeyTeplyakov/ErrorProne.NET/tree/master/docs/Rules/ERP042.md) | EventSource implementation is not correct |

### Disposable ownership

The `ErrorProne.Net.Annotations` source-generator package embeds internal
attributes directly in each project's root namespace, without adding a runtime
DLL. Public API contracts remain visible to consuming analyzers through metadata.
See [source-embedded annotations](src/ErrorProne.NET.Annotations/README.md) for
package setup, namespace overrides, and friend-assembly behavior.

Ownership analysis uses attributes and bounded inference: a caller trusts a
transfer, and the consuming method is checked for disposal or further transfer.
Third-party contracts can be supplied as `*.ownership.xml` additional files.
Use `DoNotDispose` for borrowed values, including `[return: DoNotDispose]` on
borrowed results; disposing or transferring them reports ERP046.
Unannotated results are ownership-oblivious by default. A simple, non-overridable
source method directly returning a new disposable can establish ownership;
shared, complex, or external results need an explicit contract to establish a
caller obligation. Unknown does not imply `DoNotDispose`.
The rules intentionally do not attempt a complete borrow checker or proof of
exception safety; see the documented limitations. This first version focuses
on contracts and simple inference. Dedicated diagnostics for unknown ownership
escapes are deferred; missing knowledge is not itself evidence of unsafe code.

| Id | Description |
|---|---|
| [ERP044](docs/Rules/ERP044.md) | Dispose owned resources or transfer their ownership |
| [ERP045](docs/Rules/ERP045.md) | Invalid external ownership annotation |
| [ERP046](docs/Rules/ERP046.md) | Obvious use after disposal/transfer or misuse of explicit borrowing |

### Concurrency

| Id | Description |
Expand Down
9 changes: 9 additions & 0 deletions docs/Rules/EPC34.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ This analyzer detects when methods marked with `MustUseResultAttribute` have the

The analyzer warns when the return value of a method decorated with `MustUseResultAttribute` is not used. This attribute indicates that the method's return value contains important information that should always be observed by the caller.

## Getting the attribute

The [ErrorProne.Net.Annotations source generator](../../src/ErrorProne.NET.Annotations/README.md)
provides `MustUseResultAttribute` and the recognized compatibility name
`MustUseReturnValueAttribute`. It embeds internal types in the project's root
namespace without adding a runtime annotation DLL. Attributes on public APIs
remain visible to downstream analyzers through assembly metadata.
Applications can also continue defining their own attributes, as shown below.

## Code that triggers the analyzer

```csharp
Expand Down
250 changes: 250 additions & 0 deletions docs/Rules/ERP044.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# ERP044 - Dispose owned resources before losing scope

This rule looks for recognized disposal or ownership transfer of an acquired
`IDisposable` or `IAsyncDisposable` resource. A diagnostic means the modeled
obligation was not discharged; it is not proof that a runtime leak occurs.

```csharp
var resource = new Resource(); // ERP044: no recognized disposal or transfer.
```

Dispose it with `using`, `await using`, `Dispose`, or `DisposeAsync`, or transfer
responsibility to an owning member, return value, `out`/`ref` output, or consuming
API.

## First-version scope

This version focuses on ownership contracts, bounded local/callee inference,
and obvious contract violations. It does not perform general escape analysis
or issue a separate diagnostic when ownership tracking becomes unknown.

An unannotated API may genuinely take ownership even when the analyzer cannot
infer that transfer. Describe that boundary with `AcquiresOwnership` or an
external annotation rather than treating the diagnostic as proof that the API
is unsafe. Conversely, no diagnostics does not establish safety: recognized
ownership boundaries are trusted, not verified transitively.

Dedicated, opt-in diagnostics for unknown escapes are deferred to a later
iteration. They should distinguish missing knowledge from an ownership bug.

## Ownership contracts

Attributes are recognized by their short class names. They can be declared in
your application; no runtime reference to the analyzer assembly is required.

The [ErrorProne.Net.Annotations source generator](../../src/ErrorProne.NET.Annotations/README.md)
provides these types without another runtime DLL. It generates internal
attributes in the consuming project's `RootNamespace`, with an optional
`ErrorProneAnnotationsNamespace` override. Internal attribute usages on public
APIs survive in both compiled libraries and reference assemblies, where a
downstream analyzer can read them without referencing the generator package.

Alternatively, applications can define the attributes themselves:

```csharp
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class AcquiresOwnershipAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field
| AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.ReturnValue)]
public sealed class DoNotDisposeAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property
| AttributeTargets.ReturnValue)]
public sealed class ReturnsOwnershipAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property
| AttributeTargets.ReturnValue)]
public sealed class KeepsOwnershipAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field
| AttributeTargets.Property)]
public sealed class NoOwnershipAttribute : Attribute { }
```

An acquiring parameter transfers responsibility from the caller. The caller
trusts the contract even if the implementation is incorrect; the implementation
gets the warning instead.

```csharp
void Caller()
{
var resource = new Resource();
Consume(resource); // Caller has transferred ownership.
}

void Consume([AcquiresOwnership] Resource resource) // ERP044 on resource
{
// Must dispose resource or forward responsibility.
}
```

Every acquiring parameter is checked, including constructor and indexer parameters.
Overrides and interface implementations inherit ownership contracts.

`ReturnsOwnership` marks an owning method/property result. `DoNotDispose`
marks a borrowed parameter, member, or result; prefer `[return: DoNotDispose]`
for method results. The recipient must not dispose the resource or transfer it
to an owning consumer. Return-target attributes are recognized. Explicit
contracts take priority over factory/fluent heuristics.
A borrowed-result contract does not imply that the result aliases the receiver
or any particular argument.

`KeepsOwnership` remains supported for borrowed results, and `NoOwnership`
remains supported for borrowed parameters or members. Assigning a resource to
a borrowed member does not discharge responsibility. See [ERP046](ERP046.md)
for disposal and transfer restrictions, including simple local aliases.

Parameter and return contracts are independent. Returning a borrowed value
does not suppress cleanup obligations for acquired inputs, whether borrowing
is expressed through `DoNotDispose`, `KeepsOwnership`, or an external contract.

```csharp
[return: DoNotDispose]
Resource BorrowOther(
[AcquiresOwnership] Resource owned, // ERP044: still needs disposal or transfer.
[DoNotDispose] Resource borrowed)
{
return borrowed;
}
```

Disposing `owned` before returning, or removing that acquired parameter, leaves
no unfulfilled input obligation. Returning the acquired resource itself also
counts as a transfer in this bounded model, so an explicit `ReleaseOwnership`
helper remains supported. This is a trusted boundary, not a proof that the
resource will eventually be disposed.

## Ownership-oblivious APIs

A parameter or result is **ownership-oblivious** (or **dispose-oblivious**)
when it has no explicit ownership contract from source attributes, inherited
contracts, or external annotations. This terminology follows the analogy of
nullable-oblivious APIs: missing contract information is neither an explicit
owning contract nor an explicit borrowing contract. A method can have an
annotated input and an oblivious result, or vice versa.

Disposability and ownership answer different questions. `IDisposable` and
`IAsyncDisposable` describe cleanup capabilities; they do not identify who is
responsible for a particular instance:

```csharp
static Resource Create() => new Resource();
static Resource GetShared() => shared;
```

Both results have the same disposable type and lack an explicit ownership
contract. The type alone cannot distinguish a new instance from a shared or
reused one. Moreover, an existing instance can legitimately be transferred to
a new owner: allocation freshness is not itself an ownership contract.
Annotate the boundary to express the intended responsibility:

```csharp
[return: ReturnsOwnership]
static Resource Create() => new Resource();

[return: DoNotDispose]
static Resource GetShared() => shared;
```

A future type-level "must dispose" annotation could describe the cleanup
obligation of an owner, but would not establish that every recipient owns an
instance. Type-level ownership annotations, including equivalent external
annotations, are not implemented in this version.

Oblivious describes the missing contract, not a promise that analysis is
disabled. Results default to unknown, but the bounded inference below recognizes
the direct fresh return in `Create`. Its caller must dispose or transfer that
result. `GetShared` remains unknown: merely returning a disposable type does
not create an obligation or prohibit disposal.

An unannotated property result likewise creates no obligation and is not
automatically subject to `DoNotDispose`. Unknown is not the same as borrowed.
Without an explicit borrowing contract, disposal of an unknown result is not
itself an ERP046 violation. The absence of a diagnostic does not establish safety.

## Good-enough inference

The analysis intentionally favors useful, bounded inference over an exhaustive
borrow checker:

- Creating a tracked disposable resource creates a cleanup obligation.
- Unannotated method results default to unknown. Ownership is inferred only for
a non-overridable source method in the current compilation with an expression
body or a single `return` statement directly constructing a tracked disposable.
Ordinary built-in casts and parentheses are accepted; user-defined and dynamic
conversions are not evidence of a fresh result. A direct `new T()` requires a
disposable constraint. Explicit source and external contracts take precedence.
- The same result contract or inference applies after `await`, including framework
`ConfigureAwait` chains. Explicit property contracts also apply to awaited
`Task<T>` and `ValueTask<T>` results. An async method directly returning a new
disposable can therefore establish an obligation for its awaited result.
An explicit result contract on a framework wrapper takes precedence over
the underlying member's contract.
- Mixed/conditional returns, returns through locals, factory forwarding,
additional body statements, iterator yields, overridable methods, and bodies
outside the current compilation do not establish result ownership through
this inference. Use an explicit result contract for such boundaries.
- Unannotated property access does not create a cleanup obligation. Use
`ReturnsOwnership` for owning results and `DoNotDispose` for explicit borrowing.
- Same-type instance fluent methods and generic identity-like extension methods
normally preserve the receiver/argument unless an explicit result contract or
fresh-return inference says otherwise. A simple `Clone() => new Resource()`
creates a separate obligation; disposing that copy does not dispose its receiver.
- For source callees in the current compilation, simple disposal, using, storing
in an owning member, and forwarding to another consumer can infer an acquiring
parameter. Simple local aliases are followed. Recursive inference is capped
at four parameter hops.
- `StreamReader`, `StreamWriter`, `BinaryReader`, and `BinaryWriter` constructors
are recognized as owning their stream when `leaveOpen` is absent or constant
`false`. Unknown `leaveOpen` values do not prove transfer.
- Returning a resource, assigning it to an owning field/property or an output
parameter, and `Interlocked.Exchange` into an owning member transfer ownership.
- Local aliases retain the same obligation. Discarding an alias does not transfer
it. Unconditional reassignment of a local or parameter does not let cleanup of
the new resource hide an older leaked resource. Conditional reassignments are
handled conservatively and can hide leaks.
- `using` and `await using` capture the resource before their body executes.
Reassigning the original variable inside the body does not dispose the new
value. The framework's `IAsyncDisposable.ConfigureAwait` wrapper preserves
resource identity, including through local aliases.
- User-defined conversions are separate input/result boundaries, not aliases.
Explicit conversion-operator contracts are honored independently, and source
consumption can be inferred for their parameters. Dynamic conversions do not
establish identity or prove consumption, so an input acquisition can still
have an outstanding obligation even when its converted result is disposed.

Task objects, `StringReader`, and `MemoryStream` families retain the prototype's
disposal exemptions. This includes derived types, so these exemptions are
heuristics rather than a general statement that disposal can never matter.

## External annotations

Ownership contracts for third-party APIs can be supplied through Roslyn
`AdditionalFiles`. See [ERP045](ERP045.md) for the XML schema and validation.
External and source contracts feed the same call-site and callee analysis.

## Deliberate limits

This is not a proof of exception safety or a Rust borrow checker. Conditional
cleanup/transfer is accepted even when another path may leak. General loops,
complex aliases, closures, local-function captures, dynamic calls and collection
element ownership are not modeled exhaustively. Nested lambda/local-function
bodies are not analyzed as separate lifetimes, nor used as evidence that their
enclosing function disposes a resource.

Calling `DisposeAsync` is accepted as cleanup; the rule does not prove that its
completion is awaited. Ownership carried through arbitrary task/container
wrappers is not tracked.

Transferring to a member trusts the receiving object's ownership boundary;
the rule does not prove that the containing type eventually disposes its fields.
Similarly, contracts in referenced assemblies cannot be checked against bodies
that are unavailable in the current compilation.

## Configuration

```ini
[*.cs]
dotnet_diagnostic.ERP044.severity = warning
```
Loading
Loading