Skip to content

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

Description

@eiriktsarpalis

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:

// Library assembly
public closed class Shape { }
public sealed class Circle : Shape { }
internal sealed class Hidden : Shape { }   // less accessible than the public base
// 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.

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) infers a $type discriminator (nameof(DerivedType)) for every derived type recorded in [IsClosedType(DerivedTypes = …)] (runtime#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 already notes that a generic subtype can be unspeakable for a given base instantiation — e.g. class D2<V> : C<V[]> under closed class C<T>, where no D2<…> pattern can be written for an existential C<X>. 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.

Metadata

Metadata

Assignees

Labels

Proposal QuestionQuestion to be discussed in LDM related to a proposal

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions