Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-8524] Shadowing in protocols leads to difficult to find bugs #51044

Open
gwynne opened this issue Aug 13, 2018 · 1 comment
Open

[SR-8524] Shadowing in protocols leads to difficult to find bugs #51044

gwynne opened this issue Aug 13, 2018 · 1 comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself

Comments

@gwynne
Copy link
Contributor

gwynne commented Aug 13, 2018

Previous ID SR-8524
Radar None
Original Reporter @gwynne
Type Bug
Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug
Assignee None
Priority Medium

md5: ac3d620179bbdcdd2af52327cd39dbbe

relates to:

  • SR-214 Swift 2: Guard let can shadow func param but not let-bound local constant
  • SR-1687 Swift does not warn about shadowing
  • SR-2758 Shadow stdlib method without warning
  • SR-4174 Compiler should emit warning when shadowing bound variable during pattern matching
  • SR-4444 Shadowing and order dependency
  • SR-6613 Warn when shadowing an instance variable with a local variable
  • SR-6636 Constant and Variable shadowing produces confusing Program Bugs
  • SR-1420 Provide compiler warning when shadowing generic type
  • SR-1841 QoI: Warn when shadowing generic type parameters
  • SR-3496 Silent parameter shadowing is a source of subtle, hard to spot bugs

Issue Description:

Consider the following code:

/// An object managed by some framework in use
/// The key paths point to the properties the framework manages
/// The name key path is optional, the framework won't bother if it's nil
public protocol FrameworkManagedSprocket {
    static var sprocketUUIDKey: WritableKeyPath<Self, UUID> { get }
    static var sprocketNameKey: WritableKeyPath<Self, String>? { get }
}

extension FrameworkManagedSprocket {
    static var sprocketNameKey: WritableKeyPath<Self, String>? { return nil }

    internal var _frameworkSpcketUUID: UUID {
        get { return self[keyPath: Self.sprocketUUIDKey] }
        set { self[keyPath: Self.sprocketUUIDKey] = newValue }
    }
    
    internal var _frameworkSprocketName: String? {
        get { return Self.sprocketNameKey.map { self[keyPath: $0 } }
        set { if let k = Self.sprocketNameKey { self[keyPath: k] = newValue! } }
    }
}

If it were desirable to force a given set of conforming objects to always have a name if they also conformed to some other protocol, one might try this:

public protocol FrameworkManagedWidget: FrameworkManagedSprocket {
    static var sprocketNameKey: WritableKeyPath<Self, String> { get }
}

Unfortunately, this does not have the desired effect:

final class MyWidget: FrameworkManagedWidget {
    static let sprocketUUIDKey: WritableKeyPath<Self, UUID> = \.uuid
    static let sprocketNameKey: WritableKeyPath<Self, String> = \.name
    var uuid: UUID = UUID()
    var name: String = "hello"
}
print(MyWidget()._frameworkSprocketName)
// prints "nil"

The non-optional version of the name key path shadows the optional version; it is no longer possible to access the optional version in any structure that conforms to the "widget" protocol, while the accessors for the original "sprocket" protocol still access only the optional version. The compiler never emits any errors or warnings of any kind for this usage.

This leads to very subtle and difficult-to-trace bugs for anyone unfamiliar with these semantics or the codebase in use. While this example is definitely contrived in order to show how easy it is to create such an issue by mistake, several instances of this behavior appear in the wild; there is likely to be code which even unwittingly depends on this confusing behavior.

Like the many other forms of shadowing mentioned in other bugs, this seems worthy of, at the very least, a warning from the compiler that access to the original property (and any value it may have acquired by protocol extension or otherwise) has been lost.

@gwynne
Copy link
Contributor Author

gwynne commented Aug 13, 2018

Notably, this can happen through "sideways" shadowing as well as direct inheritance:

protocol FrameworkManagedWidget { // does not require conformance to Sprocket
    static var sprocketNameKey: WritableKeyPath<Self, String> { get }
}

The same result is seen in this variation - the Sprocket version of the keypath is silently shadowed.

Worse, if there is no default implementation for either version of the keypath (e.g. if there were no extension of the Sprocket protocol), the MyWidget class becomes impossible to compile; it can only conform to one of the conflicting declarations for sprocketNameKey at a time. In this event it reduces to a case of poor compiler diagnostics - the compiler will helpfully say "Type does not conform to protocol" for one or the other protocol, and proceed to offer a fixit that creates protocol stubs that themselves don't compile, rather than diagnosing that the two protocols are mutually incompatible.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself
Projects
None yet
Development

No branches or pull requests

1 participant