Skip to content

Commit 5f8e777

Browse files
committed
Add migration paths
1 parent 8268dd9 commit 5f8e777

File tree

1 file changed

+160
-8
lines changed

1 file changed

+160
-8
lines changed

proposals/NNNN-extensible-enums.md

+160-8
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ With the following proposed solution we want to achieve the following goals:
168168
language mode so they can start declaring **new** extensible enumerations
169169
3. Provide a migration path to the new behavior without forcing new SemVer
170170
majors
171+
4. Provide tools for developers to treat dependencies as source stable
171172

172173
We propose to introduce a new language feature `ExtensibleEnums` that aligns the
173174
behavior of enumerations in both language dialects. This will make **public**
@@ -255,6 +256,114 @@ The behavior of `swift package diagnose-api-breaking-changes` is also updated
255256
to understand if the language feature is enabled and only diagnose new enum
256257
cases as a breaking change in non-frozen enumerations.
257258

259+
### Migration paths
260+
261+
The following section is outlining the migration paths and tools we propose to
262+
provide for different kinds of projects to adopt the proposed feature. The goal
263+
is to reduce churn across the ecosystem while still allowing us to align the
264+
default behavior of enums. There are many scenarios why these migration paths
265+
must exist such as:
266+
267+
- Projects split up into multiple packages
268+
- Projects build with other tools than Swift PM
269+
- Projects explicitly vendoring packages without wanting to modify the original
270+
source
271+
- Projects that prefer to deal with source breaks as they come up rather than
272+
writing source-stable code
273+
274+
#### Semantically versioned packages
275+
276+
Semantically versioned packages are the primary reason for this proposal. The
277+
expected migration path for packages when adopting the proposed feature is one
278+
of the two:
279+
280+
- API stable adoption by turning on the feature and marking all existing public
281+
enums with `@frozen`
282+
- API breaking adoption by turning on the feature and tagging a new major if the
283+
public API contains enums
284+
285+
### Projects with multiple non-semantically versioned packages
286+
287+
A common project setup is splitting the code base into multiple packages that
288+
are not semantically versioned. This can either be done by using local packages
289+
or by using _revision locked_ dependencies. The packages in such a setup are
290+
often considered part of the same logical collection of code and would like to
291+
follow the same source stability rules as same module or same package code. We
292+
propose to extend then package manifest to allow overriding the package name
293+
used by a target.
294+
295+
```swift
296+
extension SwiftSetting {
297+
/// Defines the package name used by the target.
298+
///
299+
/// This setting is passed as the `-package-name` flag
300+
/// to the compiler. It allows overriding the package name on a
301+
/// per target basis. The default package name is the package identity.
302+
///
303+
/// - Important: Package names should only be aligned across co-developed and
304+
/// co-released packages.
305+
///
306+
/// - Parameters:
307+
/// - name: The package name to use.
308+
/// - condition: A condition that restricts the application of the build
309+
/// setting.
310+
public static func packageName(_ name: String, _ condition: PackageDescription.BuildSettingCondition? = nil) -> PackageDescription.SwiftSetting
311+
}
312+
```
313+
314+
This allows to construct arbitrary package _domains_ across multiple targets
315+
inside a single package or across multiple packages. When adopting the
316+
`ExtensibleEnums` feature across multiple packages the new Swift setting can be
317+
used to continue allowing exhaustive matching.
318+
319+
While this setting allows treating multiple targets as part of the same package.
320+
This setting should only be used across packages when the packages are
321+
both co-developed and co-released.
322+
323+
### Other build systems
324+
325+
Swift PM isn't the only system used to create and build Swift projects. Build
326+
systems and IDEs such as Bazel or Xcode offer support for Swift projects as
327+
well. When using such tools it is common to split a project into multiple
328+
targets/modules. Since those targets/modules are by default not considered to be
329+
part of the package, when adopting the `ExtensibleEnums` feature it would
330+
require to either add an `@unknown default` when switching over enums defined in
331+
other targets/modules or marking all public enums as `@frozen`. Similarly, to
332+
the above to avoid this churn we recommend specifying the `-package-name` flag
333+
to the compiler for all targets/modules that should be considered as part of the
334+
same unit.
335+
336+
### Escape hatch
337+
338+
There might still be cases where developers need to consume a module that is
339+
outside of their control which adopts the `ExtensibleEnums` feature. For such
340+
cases we propose to introduce a flag `--assume-source-stable-package` that
341+
allows assuming modules of a package as source stable. When checking if a switch
342+
needs to be exhaustive we will check if the code is either in the same module,
343+
the same package, or if the defining package is assumed to be source stable.
344+
This flag can be passed multiple times to define a set of assumed-source-stable
345+
packages.
346+
347+
```swift
348+
// a.swift inside Package A
349+
public enum MyEnum {
350+
case foo
351+
case bar
352+
}
353+
354+
// b.swift inside Package B compiled with `--assume-source-stable-package A`
355+
356+
switch myEnum { // No @unknown default case needed
357+
case .foo:
358+
print("foo")
359+
case .bar:
360+
print("bar")
361+
}
362+
```
363+
364+
In general, we recommend to avoid using this flag but it provides an important
365+
escape hatch to the ecosystem.
366+
258367
## Source compatibility
259368

260369
- Enabling the language feature `ExtensibleEnums` in a module compiled without
@@ -304,6 +413,14 @@ dependency graph. This would allow a package to adopt the new language feature,
304413
break their existing, and release a new major while having minimal impact on
305414
the larger ecosystem.
306415

416+
### Using `--assume-source-stable-packages` for other diagnostics
417+
418+
During the pitch it was brought up that there are more potential future
419+
use-cases for assuming modules of another package as source stable such as
420+
borrowing from a declaration which distinguishes between a stored property and
421+
one written with a `get`. Such features would also benefit from the
422+
`--assume-source-stable-packages` flag.
423+
307424
## Alternatives considered
308425

309426
### Provide an `@extensible` annotation
@@ -329,11 +446,46 @@ resilient modules.
329446

330447
We considered introducing an annotation that allows developers to mark
331448
enumerations as pre-existing to the new language feature similar to how
332-
`@preconcurrency` works. The problem with such an annotation is how the compiler
333-
would handle this in consuming modules. It could either downgrade the warning
334-
for the missing `@unknown default` case or implicitly synthesize one. However,
335-
the only reasonable behavior for synthesized `@unknown default` case is to
336-
`fatalError`. Furthermore, such an attribute becomes even more problematic to
337-
handle when the module then extends the annotated enum; thus, making it possible
338-
to hit the `@unknown default` case during runtime leading to potentially hitting
339-
the `fatalError`.
449+
`@preconcurrency` works. Such an annotation seems to work initially when
450+
existing public enumerations are marked as `@preEnumExtensibility` instead of
451+
`@frozen`. It would result in the error about the missing `@unknown default`
452+
case to be downgraded as a warning. However, such an annotation still doesn't
453+
allow new cases to be added since there is no safe default at runtime when
454+
encountering an unknown case. Below is an example how such an annotation would
455+
work and why it doesn't allow existing public enums to become extensible.
456+
457+
```swift
458+
// Package A
459+
public enum Foo {
460+
case foo
461+
}
462+
463+
// Package B
464+
switch foo {
465+
case .foo: break
466+
}
467+
468+
// Package A adopts ExtensibleEnums feature and marks enum as @preEnumExtensibility
469+
@preEnumExtensibility
470+
public enum Foo {
471+
case foo
472+
}
473+
474+
// Package B now emits a warning downgraded from an error
475+
switch foo { // warning: Enum might be extended later. Add an @unknown default case.
476+
case .foo: break
477+
}
478+
479+
// Later Package A decides to extend the enum
480+
@preEnumExtensibility
481+
public enum Foo {
482+
case foo
483+
case bar
484+
}
485+
486+
// Package B didn't add the @unknown default case yet. So now we we emit a warning and an error
487+
switch foo { // error: Unhandled case bar & warning: Enum might be extended later. Add an @unknown default case.
488+
case .foo: break
489+
}
490+
491+
```

0 commit comments

Comments
 (0)