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
85 changes: 85 additions & 0 deletions .github/skills/ownership-adoption/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
name: ownership-adoption
description: >-
Plan and run a staged adoption of ErrorProne.NET disposable ownership in an
existing C# repository. Use for local NuGet pilots, disposable-usage audits,
finding annotation candidates, or evaluating ERP044, ERP045, and ERP046 before
rollout. Coordinate inventory, contract review, and real-project validation.
---

# Disposable ownership adoption

Adoption is an evidence-gathering exercise, not a request to put `using` around
every disposable value. An analyzer finding is an unfulfilled modeled obligation,
not proof of a runtime leak. A clean build is not a proof of ownership safety.

## Establish the boundary

Read repository instructions and record the checkout, commit, branch, dirty
files, build entry points, compiler version, and existing analyzer configuration.
Find these facts yourself; ask the user only for material decisions:

- Is this inventory-only, a package/configuration pilot, or an approved source fix?
- May local branches be created, and which files/projects may change?
- Should the review include real risks outside the current analyzer's coverage?

For a large repository, agree on a broad inventory plus deeply traced, ranked
candidates. Do not quietly substitute a few interesting files for an inventory.
Independent repositories may be investigated in parallel; keep source-analysis
ownership separate from package/build work to avoid duplicated investigation.

Keep proprietary source, paths, service names, findings, and build logs in the
consumer repository or an explicitly private artifact directory. Reusable skills
and public analyzer regressions must use independently written, generic examples.
Do not publish, commit, push, change global NuGet settings, or run live integration
tests without the corresponding authorization.

## Workflow

1. Read [ERP044](../../../docs/Rules/ERP044.md),
[ERP045](../../../docs/Rules/ERP045.md), and
[ERP046](../../../docs/Rules/ERP046.md). These documents are the policy authority.
2. Use [ownership-inventory](../ownership-inventory/SKILL.md) to map disposable
implementations, producer APIs, consumers, and mixed-cleanup call sites.
3. Use [ownership-pilot](../ownership-pilot/SKILL.md) to publish an immutable local
preview, establish the unchanged baseline, and exercise representative projects.
4. Use [ownership-contract-review](../ownership-contract-review/SKILL.md) to
classify findings and agree on individual ownership boundaries.
5. After source changes are authorized, annotate the smallest justified boundary,
rerun the pilot, and verify both intended diagnostics and negative controls.
6. Generalize demonstrated lessons into the adoption workflow; retain uncertainty
and untested steps rather than presenting a draft procedure as proven.

## Non-negotiable v1 distinctions

| Distinction | Adoption consequence |
| --- | --- |
| Disposable capability versus instance ownership | A disposable type does not make every result owning. |
| Unknown versus borrowed | Missing annotations do not prohibit disposal. |
| Input versus output ownership | A consumed parameter and a borrowed result are independent contracts. |
| Recognized transfer versus proven final cleanup | Member storage and explicit contracts are trusted boundaries. |
| Source inference versus library contracts | A simple fresh return may be inferred in one compilation but not across a project/assembly boundary. |
| Diagnostic versus defect | Missing knowledge about a task, callback, collection, or wrapper can produce a finding on correct code. |
| No diagnostic versus safety | Exceptional paths, lifecycle teardown, and general escapes remain outside a full proof. |

Do not expand the ownership model or invent type-wide attributes as an adoption
shortcut. Separate necessary analyzer improvements from consumer-code changes.

## Handoff

Deliver the package IDs/version/feed, reproducible pilot commands, baseline and
pilot outcomes, inventory coverage and gaps, and a ranked review queue. Each
candidate needs a producer, representative callers, the likely lifetime owner,
confidence, current analyzer coverage, and the next decision.

Use a concrete question, for example:

```csharp
using var first = provider.Open();
var second = provider.Open();
cache.Remember(second);
```

Determine whether `Remember` owns and eventually releases its argument before
asking whether to add disposal. Present source evidence and a recommended
contract; never infer approval from a context-only reply.
150 changes: 150 additions & 0 deletions .github/skills/ownership-contract-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
name: ownership-contract-review
description: >-
Review disposable ownership boundaries before adding ErrorProne.NET source or
external annotations. Use to classify ownership-oblivious APIs, borrowing,
transfers, mixed-cleanup callers, and suspected ownership analyzer false positives.
---

# Review an ownership contract

Read the current [ERP044](../../../docs/Rules/ERP044.md),
[ERP045](../../../docs/Rules/ERP045.md), and
[ERP046](../../../docs/Rules/ERP046.md) contracts before proposing a change.

## Build a lifetime record

For one producer/consumer boundary, document:

- Which instance is created, returned, passed, stored, or shared.
- Who is responsible before the call, after success, and after failure.
- Whether the result aliases an input, a receiver, a cached value, or a fresh value.
- Who releases retained state, including replacement and shutdown paths.
- Whether source, inherited, or external contracts already apply.
- What today's analyzer actually observes versus what manual tracing establishes.

Treat task completion, callbacks, aggregate collections, certificate collections,
and configurable wrappers as boundaries to investigate. Do not add early `using`
to silence a finding if the value must remain live after the method returns.

For a composite result, trace every independently acquired underlying resource.
Disposing a returned wrapper may close a stream but leave its connection owner
unreleased. A local `using` on that connection before returning the wrapper is
not a fix; the composite needs a truthful ownership and teardown design.

## Choose the smallest truthful contract

| Established behavior | Candidate annotation |
| --- | --- |
| Result transfers cleanup responsibility to its recipient | `ReturnsOwnership` on method/property/return |
| Recipient must neither dispose nor transfer a result | `DoNotDispose`, preferably return-targeted for methods |
| Callee acquires responsibility for an argument | `AcquiresOwnership` on that parameter |
| Callee only borrows an argument | `DoNotDispose` on that parameter |
| Ownership varies with arguments or remains uncertain | Keep unknown; document the decision or model limitation |

Do not label all results of a disposable type as owned. Do not label unknown
results borrowed merely to reduce warning volume. Do not annotate generic
collections, task-completion methods, or callback APIs globally based on one
application's lifetime convention.

An array or list is not automatically an ownership contract for its elements.
Trace element cleanup in the actual aggregate owner. Likewise, successful task
publication and eventual consumption are distinct: a task can be abandoned, and
`TrySetResult` can reject a value. Do not model conditional acceptance as an
unconditional acquiring parameter.

If one local aliases a borrowed input on one branch and a newly created copy on
another, dispose only the copy. An unconditional `using` on the merged alias can
turn a real cleanup omission into disposal of somebody else's resource.

Input and result contracts remain independent:

```csharp
[return: DoNotDispose]
Resource Replace(
[AcquiresOwnership] Resource incoming,
[DoNotDispose] Resource shared)
{
StoreOwned(incoming);
return shared;
}
```

The shared result does not discharge `incoming`; `StoreOwned` must establish
that transfer. A return contract also does not assert that the result aliases a
particular argument.

Ask one concrete decision at a time. Present representative caller code,
the implementation evidence, the proposed owner, and the recommended answer.
A discussion or context-only reply is not approval to apply attributes.

## Source versus external annotations

Use source attributes for owned APIs when changes are approved. The
[annotations generator](../../../src/ErrorProne.NET.Annotations/README.md)
embeds internal types into `RootNamespace` or its explicit override; its public
API usages survive full and reference-assembly emission. No runtime annotation
DLL or public attribute mode is needed.

Use `.ownership.xml` through the consuming project's `AdditionalFiles` when
the API cannot be edited:

```xml
<ownership>
<member id="M:Example.Factory.Open" assembly="Example.Library" returns="owned" />
<member id="M:Example.Factory.GetShared" assembly="Example.Library" returns="borrowed" />
<member id="M:Example.Owner.Take(System.IDisposable)" assembly="Example.Library">
<parameter name="resource" ownership="owned" />
</member>
</ownership>
```

Obtain declaration IDs from resolved symbols, such as
`DocumentationCommentId.CreateDeclarationId`, rather than guessing overloaded,
generic, constructor, or explicit-interface syntax. Use the assembly's simple
name and actual parameter names. Verify resolution in the intended compilation:
an absent member can be legal in an annotation pack, so no ERP045 does not prove
that an entry matched. Source contracts take precedence over XML.

The v1 XML schema cannot express arbitrary conditional ownership. Never turn
`leaveOpen: true` or a conditional ownership flag into an unconditional transfer.

## Validate the decision, not just compilation

After authorization, require a positive and a negative example for the boundary:
missing cleanup reports, legitimate cleanup/transfer does not, and borrowed
cleanup reports where applicable. Check library consumers separately from
same-compilation inference. Verify independent incoming and outgoing obligations.

If the issue is in the analyzer, create an independently written minimal
regression in ErrorProne.NET. Reproduce the diagnostic before changing analysis,
then check adjacent ownership cases and the real pilot again.

Keep negative-control scenarios for retained task results, reusable release
tokens, explicit shutdown-callback cleanup, and collection-element transfers.
Known completed-task wrappers transport result identity; unknown publication,
collection arguments and captures make ownership uncertain and silence ERP044.
Distinguish these outcomes: silence at an unknown handoff does not prove
successful transfer, callback execution or eventual cleanup. Contrast each with
local abandonment, discarded completed wrappers, explicit borrowed boundaries
and ordinary receiver use. Failed-transfer cases can remain intentionally
undetected under the low-noise policy; record that trade-off instead of claiming
that a green build establishes safety.

Keep fixes to consumer lifetimes, fixes to analyzer modeling, and intentional
trusted contracts separate in the review record. Never close an uncertain case
as a confirmed leak or a proven-safe transfer.

## Record v1 gaps explicitly

Separate a supported contract that behaves incorrectly from a documented
analysis limit, missing/incorrect consumer metadata, or an integration problem
such as stale compiler output. Keep both noisy correct lifetimes and missed
unsafe lifetimes in the review queue.

For each gap, state the expected lifetime, observed diagnostic or absence,
minimal independently written reproduction, affected real-world pattern,
current documented boundary, and the decision needed before expanding v1.
Characterization tests can record a known limitation without changing policy;
name them accordingly. Passing such a test means the limitation was reproduced,
not that the example's lifetime is safe.
118 changes: 118 additions & 0 deletions .github/skills/ownership-inventory/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
name: ownership-inventory
description: >-
Inventory IDisposable and IAsyncDisposable implementations and trace ownership
across C# producers and consumers. Use to find inconsistent disposal of results,
critical resource types, ownership-transfer boundaries, and adoption candidates.
---

# Inventory disposable ownership

## Define and measure coverage

Start from tracked source and project membership. Exclude generated outputs,
build caches, secrets, deployment data, and unrelated languages. Classify
production, test-only, generated, and vendored code separately.
Account for case-varied project extensions. Inspect provenance before excluding
a directory merely named `Output`: it can contain real source. A fake in a
shipping-library folder is still a fake, not proof of production ownership.

Prefer an existing semantic index or Roslyn symbols for type relationships and
references. If only syntax/search is available, label the output accordingly.
Search is a candidate generator, not evidence that two callers invoke the same API.
Before starting an indexer or language server in a read-only audit, check whether
it triggers design-time builds, restores, or writes outside the approved scope.

Inventory:

- Direct and inherited `IDisposable` and `IAsyncDisposable` implementations.
- Disposable structs and lease/registration tokens.
- Pattern-disposable types as a separate category; verify analyzer support.
- `SafeHandle` descendants and resource-owning wrappers.
- Raw handles and manual release APIs as a separate, currently unmodeled category.
- Interfaces/abstract bases that carry contracts, separately from instantiable types.

Follow in-repository base types transitively and distinguish nested types and
generic arity. Merge partial declarations by symbol when binding is available;
otherwise preserve declaration rows and qualify any unique-type count.
Retain unresolved external bases,
ambiguous type names, conditional compilation, and unavailable projects as
coverage gaps. Do not label a direct `: IDisposable` text search exhaustive.

For every inventory entry record:

| Field | Evidence |
| --- | --- |
| Identity | Project/assembly, qualified type, arity, declaration locations |
| Classification | Production/test/vendor; direct/inherited/pattern/unknown |
| Lifetime role | Resource owner, borrowed facade, lease, aggregate, marker |
| Resource impact | Native resource, connection, timer, subscription, lock, managed-only, unknown |
| Cleanup | Actual cleanup implementation and owned fields; not merely the method name |
| Usage | Construction/factories, returned values, parameters, member storage, representative callers |
| Confidence | Semantic resolution or bounded syntax/manual tracing; unresolved edges |

## Find high-value producer/consumer pairs

Group calls by resolved member identity, overload, and relevant arguments.
Include properties and awaited `Task<T>`/`ValueTask<T>` results. Keep disposal
of a task distinct from disposal of the resource produced by awaiting it.

Look for an API whose result is:

- Disposed with `using`, `await using`, `Dispose`, or `DisposeAsync` by some callers.
- Ignored, retained in a local, stored, returned, or passed onward by other callers.
- Transferred through a wrapper, callback, task completion, queue, or collection.

For each promising pair, trace both call paths to a terminal owner. Check whether
the API allocates, returns shared state, lends a pooled object, or transfers an
existing object. Freshness is evidence, not a substitute for an ownership contract.

```csharp
using var a = sessions.Get();
var b = sessions.Get();
owner.Attach(b);
```

This is not a leak report until `Get`, `Attach`, and the owner's teardown have
been investigated. Repeated calls may return the same object, and one caller's
existing `using` may be the bug.

Caching does not by itself prove borrowing. A cached release token can represent
one disposal duty for each successful lock acquisition, even when several
acquisitions receive the same object. Conversely, constructing that token may not
acquire anything. Trace acquisition/release state, not just allocation and identity.

Inspect ownership-changing arguments such as `leaveOpen`, `disposeHandler`, and
pool/retention options. Do not generalize one overload or argument combination
into an unconditional contract for every call.

## Rank without inflating certainty

Prioritize resource cost, call frequency, retention duration, and affected
production paths. A cancellation source that owns a timer or registrations is
different evidence from an otherwise unused disposable marker.

Record at least one negative control: a mixed-use pattern that is safe because
of sharing or a verified transfer. Include candidates where `DoNotDispose`
would prevent an inappropriate cleanup, not only missing-dispose candidates.

Separate results into:

1. Evidence-backed missing cleanup or misuse.
2. Correct runtime ownership with missing analyzer-visible contracts.
3. Unresolved ownership requiring an API-owner decision.
4. Analyzer false positive or unsupported transport.
5. Real risk outside v1, such as exceptional-path or lifecycle cleanup.

Deduplicate multi-target findings by rule, source location, and modeled resource;
retain the target frameworks as evidence rather than counting each as a new bug.

## Deliverables

Save a private machine-readable inventory and a code-linked review queue.
State files/projects considered, direct and transitive discovery methods,
classification counts, unresolved bases, and manually traced coverage.
Do not confuse the number of matches with the number of audited lifetimes.

Feed the strongest candidates into
[ownership-contract-review](../ownership-contract-review/SKILL.md).
Loading
Loading