Skip to content

Commit 70cc202

Browse files
committed
Add migration paths
1 parent 8268dd9 commit 70cc202

File tree

1 file changed

+159
-8
lines changed

1 file changed

+159
-8
lines changed

proposals/NNNN-extensible-enums.md

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

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

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

309425
### Provide an `@extensible` annotation
@@ -329,11 +445,46 @@ resilient modules.
329445

330446
We considered introducing an annotation that allows developers to mark
331447
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`.
448+
`@preconcurrency` works. Such an annotation seems to work initially when
449+
existing public enumerations are marked as `@preEnumExtensibility` instead of
450+
`@frozen`. It would result in the error about the missing `@unknown default`
451+
case to be downgraded as a warning. However, such an annotation still doesn't
452+
allow new cases to be added since there is no safe default at runtime when
453+
encountering an unknown case. Below is an example how such an annotation would
454+
work and why it doesn't allow existing public enums to become extensible.
455+
456+
```swift
457+
// Package A
458+
public enum Foo {
459+
case foo
460+
}
461+
462+
// Package B
463+
switch foo {
464+
case .foo: break
465+
}
466+
467+
// Package A adopts ExtensibleEnums feature and marks enum as @preEnumExtensibility
468+
@preEnumExtensibility
469+
public enum Foo {
470+
case foo
471+
}
472+
473+
// Package B now emits a warning downgraded from an error
474+
switch foo { // warning: Enum might be extended later. Add an @unknown default case.
475+
case .foo: break
476+
}
477+
478+
// Later Package A decides to extend the enum
479+
@preEnumExtensibility
480+
public enum Foo {
481+
case foo
482+
case bar
483+
}
484+
485+
// Package B didn't add the @unknown default case yet. So now we we emit a warning and an error
486+
switch foo { // error: Unhandled case bar & warning: Enum might be extended later. Add an @unknown default case.
487+
case .foo: break
488+
}
489+
490+
```

0 commit comments

Comments
 (0)