@@ -168,6 +168,7 @@ With the following proposed solution we want to achieve the following goals:
168
168
language mode so they can start declaring ** new** extensible enumerations
169
169
3 . Provide a migration path to the new behavior without forcing new SemVer
170
170
majors
171
+ 4 . Provide tools for developers to treat dependencies as source stable
171
172
172
173
We propose to introduce a new language feature ` ExtensibleEnums ` that aligns the
173
174
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
255
256
to understand if the language feature is enabled and only diagnose new enum
256
257
cases as a breaking change in non-frozen enumerations.
257
258
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
+
258
366
## Source compatibility
259
367
260
368
- 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,
304
412
break their existing, and release a new major while having minimal impact on
305
413
the larger ecosystem.
306
414
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
+
307
423
## Alternatives considered
308
424
309
425
### Provide an ` @extensible ` annotation
@@ -329,11 +445,46 @@ resilient modules.
329
445
330
446
We considered introducing an annotation that allows developers to mark
331
447
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