@@ -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,114 @@ 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 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
+
258
367
## Source compatibility
259
368
260
369
- 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,
304
413
break their existing, and release a new major while having minimal impact on
305
414
the larger ecosystem.
306
415
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
+
307
424
## Alternatives considered
308
425
309
426
### Provide an ` @extensible ` annotation
@@ -329,11 +446,46 @@ resilient modules.
329
446
330
447
We considered introducing an annotation that allows developers to mark
331
448
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