dotnet / dotnet/csharplang

[Proposal question]: Should the immediate descendants of a `closed` type be required to have the same accessibility as the base?

Open
#10,237 6 comments 0 reactions 1 assignee Claimed by @RikkiGibson View on GitHub
Proposal Question
Dominant language
C#
Stars
12.7k
Forks
1.1k
Avg merge
11h 1m
Merged PRs (30d)
3

Description

- Champion:
- Spec section: [*Exhaustiveness when a subtype can't be used*](https://github.com/dotnet/csharplang/blob/main/proposals/closed-hierarchies.md#exhaustiveness-when-a-subtype-cant-be-used)
- Prior LDM decision (2026-04-20): [*Accessibility and exhaustiveness*](https://github.com/dotnet/csharplang/blob/ffabcf2ebd738c759d1d80dcfbd20645040fde2f/meetings/2026/LDM-2026-04-20.md#accessibility-and-exhaustiveness)
- Related: [dotnet/runtime#129041](https://github.com/dotnet/runtime/issues/129041) (STJ support), [dotnet/runtime#129009](https://github.com/dotnet/runtime/issues/129009) (`[IsClosedType]`)

## The problem

A `closed` class restricts derivation to its declaring assembly so that a `switch` over its direct descendants can be treated as exhaustive. Today a direct subtype may be **less accessible** than the `closed` base, in which case a consumer that can see the base but not the subtype can never write an exhaustive switch:

```cs
// Library assembly
public closed class Shape { }
public sealed class Circle : Shape { }
internal sealed class Hidden : Shape { } // less accessible than the public base
```

```cs
// Consumer assembly
string Describe(Shape shape) => shape switch
{
Circle => "circle",
// warning: switch is non-exhaustive. Pattern 'Shape' is not handled.
// But 'Hidden' is internal to the library and cannot be named here, so the consumer
// can never exhaust the switch by matching the derived types.
};
```

The current proposal accepts this and reports it only at the use site (the spec section linked above). LDM considered a declaration-time accessibility restriction on **2026-04-20** and declined: *"We will not add any new restrictions related to accessibility."*

## The question

Should we revisit that decision and require, at declaration time, that every immediate descendant of a `closed` type have the **same declared accessibility** as the base?

## Proposed rule

> Every immediate descendant (direct subtype) of a `closed` type must be declared with the same declared accessibility as the closed base type; any other accessibility is a compile-time error.

```cs
public closed class Shape { }

public sealed class Circle : Shape { } // ok: same accessibility as 'Shape'
internal sealed class Hidden : Shape { } // error: a direct subtype of a closed type must have
// the same accessibility as its base
```

## Considerations in favor

- Exhaustiveness then holds for **every** consumer that can see the base — which is the point of `closed`.
- A declaration-time error is actionable at the hierarchy, versus a non-exhaustiveness warning in distant (possibly cross-assembly) consumer code that the consumer cannot fix.
- The direct descendants are part of the type's contract; hiding cases from consumers who can see the base contradicts what `closed` advertises.
- STJ's closed-hierarchy support (`JsonSerializerOptions.InferClosedTypePolymorphism`, [runtime#129041](https://github.com/dotnet/runtime/issues/129041)) infers a `$type` discriminator (`nameof(DerivedType)`) for every derived type recorded in `[IsClosedType(DerivedTypes = …)]` ([runtime#129009](https://github.com/dotnet/runtime/issues/129009)); permitting less-accessible descendants means an untrusted payload could instantiate an otherwise-inaccessible type/constructor — a potential security concern, which STJ is likely to decline to support regardless.

## Scope and related considerations

- **`file`-local closed classes.** `file` is a separate modifier, not a named accessibility level, so it needs explicit treatment: a `file` `closed` base should require `file`-local direct subtypes (and a more-visible base cannot have a `file` subtype). The current implementation has not exercised `file closed class`; that gap needs coverage regardless of the outcome here.
- **Unspeakable or constraint-adding generic subtypes.** The spec's [*Exhaustiveness when a subtype can't be used*](https://github.com/dotnet/csharplang/blob/main/proposals/closed-hierarchies.md#exhaustiveness-when-a-subtype-cant-be-used) already notes that a generic subtype can be *unspeakable* for a given base instantiation — e.g. `class D2 : C` under `closed class C`, where no `D2<…>` pattern can be written for an existential `C`. The same reasoning behind this question applies: guaranteeing a usable construction *wherever any base construction is used* would mean disallowing such non-speakable derived generics at declaration, effectively restricting generic subtypes to "trivial" generics — ones that pass their type parameters directly to the base and add no constraints of their own. Best treated as a separate question.
- **Consumption-site dependency on inaccessible metadata.** Reference assemblies and trimming tools would have to special-case descendants of a `closed` class to keep consumer pattern matching correct, and Roslyn metadata-import modes that hide inaccessible members interact poorly with consumption-site behavior that depends on inaccessible derived types. This is the same class of hole already known from private struct fields (managed/unmanaged determination — and thus pointer eligibility — depending on inaccessible fields); we would prefer not to enlarge it. The rule removes the dependency.
- **Nested `closed` levels.** The rule applies to each `closed` type's own direct descendants independently; deeper hierarchies stay matchable.
- **Type parameters constrained to a `closed` type.** Unaffected — that concerns generics and exhaustiveness, not accessibility.

## Options

1. **Adopt the rule as a compile-time error** — strongest guarantee; a `closed` type is always exhaustively matchable by every consumer that can see it.
2. **Adopt the rule as a warning** — flags the hazard at declaration without hard-breaking the pattern; weaker guarantee.
3. **Status quo (2026-04-20)** — no declaration-time restriction; continue reporting non-exhaustiveness only at the use site.

Sub-decision for options 1 and 2: whether the rule extends to require matching `file`-locality, as described under *Scope and related considerations*.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.