Pitch: Warn on near-miss protocol witness when a default implementation is chosen

When a type declares a same-named property whose type is a refinement of a protocol requirement’s type, and the protocol provides a default implementation in an extension, Swift silently uses the default as the witness. The refined property is then ignored in generic/existential contexts. This follows current witness-matching rules, but the lack of a diagnostic makes it an easy footgun.

Minimal reproducer:

protocol LayoutDescriptor {}
struct ConcreteLayoutDescriptor: LayoutDescriptor { var value: Int }

protocol DefaultLayoutDescribing {
    var defaultLayoutDescriptor: LayoutDescriptor { get }
}

extension DefaultLayoutDescribing {
    var defaultLayoutDescriptor: LayoutDescriptor { ConcreteLayoutDescriptor(value: 0) }
}

struct MyPresenter: DefaultLayoutDescribing {
    let defaultLayoutDescriptor = ConcreteLayoutDescriptor(value: 42)
}

let presenter: any DefaultLayoutDescribing = MyPresenter()
print("Direct:", MyPresenter().defaultLayoutDescriptor) // 42
print("Existential:", presenter.defaultLayoutDescriptor) // 0

SwiftFiddle link.

The second print uses the extension default (value: 0), not the concrete declaration (value: 42). This compiles without any warning.

Why this is a problem:

  • The developer clearly intends MyPresenter.defaultLayoutDescriptor to satisfy the protocol requirement. The name matches exactly, and ConcreteLayoutDescriptor conforms to
    LayoutDescriptor.
  • Direct access works as expected, masking the issue in some testing scenarios.
  • Without the extension default, the compiler correctly errors with "candidate has non-matching type" — so it knows these types don't match. But when a default implementation exists, it
    silently falls back to it instead of warning.

Proposal:

Emit a “near-miss” warning when a member has the same name as a protocol requirement, is not selected as the witness due to a type mismatch, and the mismatched type is a more specific (aka refined) type that could satisfy the requirement via an implicit upcast (e.g. ConcreteLayoutDescriptor vs any LayoutDescriptor). If a default implementation is used instead, warn that the default will be the witness.

Something like:

warning: property 'defaultLayoutDescriptor' with type 'ConcreteLayoutDescriptor' does not satisfy requirement with type 'LayoutDescriptor'; default implementation will be used

note: add explicit type annotation 'LayoutDescriptor' to satisfy the requirement

The compiler already has near-miss diagnostics for @objc protocol conformances. This would extend that infrastructure to cover same-name type mismatches when a
default implementation silently takes over.

Prior discussion: This was reported as SR-10695 in 2019, where Jordan Rose suggested near-miss checking for this case. The issue was closed without the diagnostic being implemented.

Real-world impact: I found some instances of this pattern in our production iOS codebase, all silently using the wrong implementation.

I'd be happy to put together a PR to add a diagnostic for this if the direction seems right.

Quite sure this has been discussed on these forums before—worth checking.

You get the near-miss checking that you want if you declare the protocol conformance in an extension and implement the requirements inside that extension. I don't remember if that near-miss checking was implemented before or after 2019. The wrinkle here is, of course, that you can't implement stored properties in extensions.

For the same reason, though, there wouldn't be a great way to silence a near-miss warning for stored properties if you did want the actual behavior of the code as written. Either (1) the warning only checks for near-misses inside primary declarations if the conformance is also declared there, in which case users would have to silence the warning by moving the conformance to an extension, losing near-miss checking for all other stored properties; or (2) the warning checks for near-misses inside primary declarations no matter where the conformance is declared, in which case it would be impossible to silence the near-miss warning.

Anyway, long story short, I think the answer is resurrecting a proposal to allow same-file extensions to declare stored properties rather than trying to work out new variations on the diagnostics we have.

If we really wanted to, I think extending the near-miss diagnostics to scenario (1) above is probably fine, but I'd want to see someone work out what that means in terms of all the special-cased rules for protocols whose conformances have to be declared on the primary declaration—specifically, whether there are any issues with composing the rules that would result in unsilenceable warnings.

2 Likes

Previously we've discussed having something like override for "this declaration is intended to witness a protocol requirement; error if it does not". I can't remember why it never happens though…

1 Like

Plenty in the forums about that, but probably well out of scope for a diagnostics QoI discussion :)

The cross-module case is what makes this compiler territory, but the same-file case (protocol, default impl, and conforming type all in one file) is catchable purely syntactically with no type resolution, just identifier comparison. Narrow, but it would cover the "canonical interview-question example" as a stopgap. If the silenceability problem is what's blocking the compiler version, an opt-in SwiftLint rule sidesteps it. For the full cross-module case, sourcekit-lsp / IndexStore-based tooling (a la Periphery) is the right shape?