@@ -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
1691693 . 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
172173We propose to introduce a new language feature ` ExtensibleEnums ` that aligns the
173174behavior 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
255256to understand if the language feature is enabled and only diagnose new enum
256257cases 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,
304413break their existing, and release a new major while having minimal impact on
305414the 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
330447We considered introducing an annotation that allows developers to mark
331448enumerations 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