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

Add rule to infer property types from the right-hand-side value rather than writing the type explicitly on the left-hand side #263

Merged
merged 7 commits into from
Jun 7, 2024
Prev Previous commit
Next Next commit
Add discussion around edge cases
  • Loading branch information
calda committed Mar 28, 2024
commit 5a39cdcb87b437dd8d9a3409b425e3646836ee28
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ _You can enable the following settings in Xcode by running [this script](resourc

// ALSO RIGHT: Explicit types can be necessary when the right-hand side has
// a different type from the one written explicitly on the left-hand side.
let nautralSatellite: PlanetaryBody? = Moon(mass: 7.347e22)
let naturalSatellite: PlanetaryBody? = Moon(mass: 7.347e22)
let moon: PlanetaryBody? = nil // nil literals are typeless
let numberOfPlanets: UInt = 8 // integer literals default to `Int`
let sunMass: CGFloat = 1.989e30 // floating-point literals default to `Double`
calda marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -400,6 +400,40 @@ _You can enable the following settings in Xcode by running [this script](resourc
let sunMass = CGFloat(1.989e30)
let planets = [Planet]()
```

There are some rarer cases where the inferred type syntax has a different meaning than the explicit type syntax. In these cases, the explicit type syntax is still permitted:

```swift
extension String {
static let earth = "Earth"
}

// RIGHT: If the property's type is optional, moving the optional type
// to the right-hand side may result in invalid code.
let planetName: String? = .earth

// WRONG: fails with "error: type 'String?' has no member 'foo'"
let planetName = String?.earth
```

```swift
struct SaturnOutline: ShapeStyle { ... }

extension ShapeStyle where Self == SaturnOutline {
static var saturnOutline: SaturnOutline {
SaturnOutline()
}
}

// RIGHT: If the property's type is an existential / protocol type, moving the type
// to the right-hand side will result in invalid code if the value is defined in an
// extension like `extension ShapeStyle where Self == SaturnOutline`.
// SwiftFormat autocorrect detects this case by checking for the existential `any` keyword.
let myShape1: any ShapeStyle = .saturnOutline
Copy link
Member Author

@calda calda Jun 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent a bit of time thinking about this case in more detail, that let myShape1: ShapeStyle = .saturnOutline is allowed but let myShape1: = ShapeStyle.saturnOutline isn't. I couldn't understand why this wasn't allowed.

This syntax was introduced in SE-0299. This question came up briefly in the review thread, and Pavel mentioned that there was more discussion in the pitch thread. A found the most compelling discussion here in the pitch thread.

They intentionally don't frame this as adding a static member to the protocol type, but rather a shorthand syntax that lets you omit the name of the concrete type.

Basically, the thinking is that this code:

let myShape1: ShapeStyle = .saturnOutline

is actually implicit shorthand for this code:

let myShape1: ShapeStyle = SaturnOutline.saturnOutline

not shorthand for this code:

let myShape1: ShapeStyle = ShapeStyle.saturnOutline

This is subtle but distinct, and is a reasonable explanation for why ShapeStyle.saturnOutline isn't allowed.

Copy link
Member Author

@calda calda Jun 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem should also go away eventually, once any is required in a future language mode (just not Swift 6).


// WRONG: fails with "error: static member 'saturnOutline' cannot be used on protocol metatype '(any ShapeStyle).Type'"
let myShape2 = (any ShapeStyle).myShape
Copy link
Member Author

@calda calda Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way -- for this we have the ability to tell SwiftFormat a list of symbols to exclude from this rule (--preservesymbols ShapeStyle,OtherType,etc). Inside the apps repo we could exclude any type or property name where we frequently use this syntax. Two relevant examples are:

  • UnresolvedShapeStyle
    • all existing callsites use any UnresolvedShapeStyle so this is already handled properly
  • canonicalFake
    • there are about a dozen examples of let experimentsService: ExperimentsService = .canonicalFake (etc). It seems like a good idea to exclude this using --preservesymbols canonicalFake. Updating the callsites to let experimentsService: any ExperimentsService = .canonicalFake also works.

Copy link
Contributor

@bachand bachand Apr 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging this @calda . I was wondering about this... My recollection is that eventually it's going to be required that we use any for any existential/protocol types but that this requirement is being slowly rolled out. Is your understanding also that eventually all existential/protocol type will need to be preceded by any? If so, I think it's less problematic if we need to work around certain situations like the above ones you highlighted for the time being.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to understand if there would be cases when we still need --preservesymbols if/when the language requires that all existential/protocol types are preceded by any.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw on the Swift Evolution forums that they decided to not make any mandatory in Swift 6 mode after all. Although after a quick search on the forums I can't actually find where they said that.

This doesn't necessarily confirm it, but if you build code like this using a nightly Swift 6 build it doesn't require any.

I am trying to understand if there would be cases when we still need --preservesymbols if/when the language requires that all existential/protocol types are preceded by any.

If all call sites used the existential any syntax, we wouldn't need to use --preservesymbols

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found the quote: https://forums.swift.org/t/progress-toward-the-swift-6-language-mode/68315

The Language Steering Group has decided that one previously-accepted upcoming feature, ExistentialAny, will not be enabled by default in Swift 6. SE-0335: Introduce existential any introduces the any keyword to identify existential types. The proposal also specifies that "bare" protocol names will no longer be permitted as types---they must either use any or some, as appropriate---under the upcoming feature flag ExistentialAny. Given the concerns about migration to consistent use of existential any in the language, and the expectation that additional language improvements will be coming that might affect the end result of that migration, the Language Steering Group is deferring the source-incompatible changes in SE-0335 to a future language revision.

It sounds like they didn't rule out this direction, but rather just deferred it for longer (probably a very long time, if we have to wait for Swift 7!)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the apps repo PR that I merged today, I included --preservesymbols canonicalFake. This is a common and reasonable pattern that I think we should continue supporting. It's not common to use the any ExperimentService spelling today, so it felt too weird to require it in this specific case due to this limitation of the linter.

```

</details>

Expand Down
Loading