Skip to content

Commit ad9acb7

Browse files
authored
Merge pull request #2673 from AnthonyLatsis/migration-tooling
Migration tooling for Swift features
2 parents da3dd1e + e4b2583 commit ad9acb7

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
# Migration tooling for Swift features
2+
3+
* Proposal: [SE-NNNN](NNNN-migratable-features.md)
4+
* Authors: [Anthony Latsis](https://github.com/AnthonyLatsis), [Pavel Yaskevich](https://github.com/xedin)
5+
* Review Manager: TBD
6+
* Status: **Awaiting implementation**
7+
* Implementation: TBD
8+
* Review: [pitch](https://forums.swift.org/t/pitch-adoption-tooling-for-upcoming-features/77936)
9+
10+
## Introduction
11+
12+
Swift 5.8 introduced [upcoming features][SE-0362], which enabled piecemeal
13+
adoption of individual source-incompatible changes that are included in a
14+
language mode.
15+
Many upcoming features have a mechanical migration, meaning the compiler can
16+
determine the exact source changes necessary to allow the code to compile under
17+
the upcoming feature while preserving the behavior of the code.
18+
This proposal seeks to improve the experience of enabling individual Swift
19+
features by providing an integrated mechanism for producing these source code
20+
modifications automatically.
21+
22+
## Motivation
23+
24+
It is the responsibility of project maintainers to preserve source (and binary)
25+
compatibility both internally and for library clients when enabling an upcoming
26+
feature, which can be difficult or tedious without having tools to help detect
27+
possibly inadvertent changes or perform monotonous migration shenanigans for
28+
you.
29+
*Our* responsibility is to make that an easier task for everybody.
30+
31+
### User intent
32+
33+
A primary limiting factor in how proactively and accurately the compiler can
34+
assist developers with adopting a feature is a lack of comprehension of user
35+
intent.
36+
Is the developer expecting guidance on adopting an improvement?
37+
All the compiler knows to do when a feature is enabled is to compile code
38+
accordingly.
39+
If an upcoming feature supplants an existing grammatical construct or
40+
invalidates an existing behavior, the language rules alone suffice because
41+
Swift can consistently infer the irrefutable need to diagnose certain code
42+
patterns just by spotting them.
43+
44+
Needless to say, not all upcoming features fall under these criteria (and not
45+
all features are source-breaking in the first place).
46+
Consider [`DisableOutwardActorInference`][SE-0401], which changes actor
47+
isolation inference rules with respect to wrapped properties.
48+
There is no way for the programmer to specify that they'd like compiler fix-its
49+
to make the existing actor isolation inference explicit.
50+
If they enable the upcoming feature, their code will simply behave differently.
51+
This was a point of debate in the review of [SE-0401], and the Language
52+
Steering Group concluded that automatic migration tooling is the right way to
53+
address this particular workflow, as
54+
[noted in the acceptance notes][SE-0401-acceptance]:
55+
56+
> the Language Steering Group believes that separate migration tooling to
57+
> help programmers audit code whose behavior will change under Swift 6 mode
58+
> would be beneficial for all upcoming features that can change behavior
59+
> without necessarily emitting errors.
60+
61+
### Automation
62+
63+
Many existing and prospective upcoming features account for simple and reliable
64+
migration paths to facilitate adoption:
65+
66+
* [`NonfrozenEnumExhaustivity`][SE-0192]: Restore exhaustivity with
67+
`@unknown default:`.
68+
* [`ConciseMagicFile`][SE-0274]: `#file``#filePath`.
69+
* [`ForwardTrailingClosures`][SE-0286]: Disambiguate argument matching by
70+
de-trailing closures and/or inlining default arguments.
71+
* [`ExistentialAny`][SE-0335]: `P``any P`.
72+
* [`ImplicitOpenExistentials`][SE-0352]: Suppress opening with `as any P`
73+
coercions.
74+
* [`BareSlashRegexLiterals`][SE-0354]: Disambiguate using parentheses,
75+
e.g. `foo(/a, b/)``foo((/a), b/)`.
76+
* [`DeprecateApplicationMain`][SE-0383]: `@UIApplicationMain``@main`,
77+
`@NSApplicationMain``@main`.
78+
* [`DisableOutwardActorInference`][SE-0401]: Specify global actor isolation
79+
explicitly.
80+
* [`InternalImportsByDefault`][SE-0409]: `import X``public import X`.
81+
* [`GlobalConcurrency`][SE-0412]: Convert the global variable to a `let`, or
82+
`@MainActor`-isolate it, or mark it with `nonisolated(unsafe)`.
83+
* [`MemberImportVisibility`][SE-0444]: Add explicit imports appropriately.
84+
* [`InferSendableFromCaptures`][SE-0418]: Suppress inference with coercions
85+
and type annotations.
86+
* [Inherit isolation by default for async functions][async-inherit-isolation-pitch]:
87+
Mark nonisolated functions with the proposed attribute.
88+
89+
Application of these adjustments can be fully automated in favor of preserving
90+
behavior, saving time for more important tasks, such as identifying, auditing,
91+
and testing code where a change in behavior is preferable.
92+
93+
## Proposed solution
94+
95+
Introduce the notion of a migration mode for individual experimental and
96+
upcoming features.
97+
The core idea behind migration mode is a declaration of intent that can be
98+
leveraged to build better supportive adoption experiences for developers.
99+
If enabling a feature communicates an intent to *enact* rules, migration mode
100+
communicates an intent to migrate code so as to preserve compatibility once the
101+
feature is enabled.
102+
103+
This proposal will support the set of existing upcoming features that
104+
have mechanical migrations, as described in the [Automation](#automation)
105+
section.
106+
All future proposals that intend to introduce an upcoming feature and
107+
provide for a mechanical migration should include a migration mode and detail
108+
its behavior alongside the migration paths in the *Source compatibility*
109+
section.
110+
111+
## Detailed design
112+
113+
Upcoming features that have mechanical migrations will support a migration
114+
mode, which is a new mode of building a project that will produce compiler
115+
warnings with attached fix-its that can be applied to preserve the behavior
116+
of the code under the feature.
117+
118+
The action of enabling a previously disabled upcoming feature in migration
119+
mode must not cause any new compiler errors or behavioral changes, and the
120+
fix-its produced must preserve compatibility.
121+
Compatibility here refers to both source and binary compatibility, as well as
122+
to behavior.
123+
Additionally, this action will have no effect if the mode is not supported
124+
for a given upcoming feature, i.e., because the upcoming feature does not
125+
have a mechanical migration.
126+
A corresponding warning will be emitted in this case to avoid the false
127+
impression that the impacted source code is compatible with the feature.
128+
This warning will belong to the diagnostic group `StrictLanguageFeatures`.
129+
130+
### Interface
131+
132+
The `-enable-*-feature` frontend and driver command line options will start
133+
supporting an optional mode specifier with `migrate` as the only valid mode:
134+
135+
```
136+
-enable-upcoming-feature <feature>[:<mode>]
137+
-enable-experimental-feature <feature>[:<mode>]
138+
139+
<mode> := migrate
140+
```
141+
142+
For example:
143+
144+
```
145+
-enable-upcoming-feature InternalImportsByDefault:migrate
146+
```
147+
148+
If the specified mode is invalid, the option will be ignored, and a warning will
149+
be emitted.
150+
This warning will belong to the diagnostic group `StrictLanguageFeatures`.
151+
In a series of either of these options applied to a given feature, only the
152+
last option will be honored.
153+
If a feature is both implied by the effective language mode and enabled in
154+
migration mode, the latter option will be disregarded.
155+
156+
### Diagnostics
157+
158+
Diagnostics emitted in relation to a specific feature in migration mode must
159+
belong to a diagnostic group named after the feature.
160+
The names of diagnostic groups can be displayed alongside diagnostic messages
161+
using `-print-diagnostic-groups` and used to associate messages with features.
162+
163+
### `swift package migrate` command
164+
165+
To enable seemless migration experience for Swift packages, I'd like to propose a new Swift Package Manager command - `swift package migrate` to complement the Swift compiler-side changes.
166+
167+
The command would accept one or more features that have migration mode enabled and optionally a set of targets to migrate, if no targets are specified the whole package is going to be migrated to use new features.
168+
169+
#### Interface
170+
171+
```
172+
USAGE: swift package migrate [<options>] --to-feature <to-feature> ...
173+
174+
OPTIONS:
175+
--targets <targets> The targets to migrate to specified set of features or a new language mode.
176+
--to-feature <to-feature>
177+
The Swift language upcoming/experimental feature to migrate to.
178+
-h, --help Show help information.
179+
```
180+
181+
#### Use case
182+
183+
```
184+
swift package migrate --targets MyTarget,MyTest --to-feature ExistentialAny
185+
```
186+
187+
This command would attempt to build `MyTarget` and `MyTest` targets with `ExistentialAny:migrate` feature flag, apply any fix-its associated with
188+
the feature produced by the compiler, and update the `Package.swift` to
189+
enable the feature(s) if both of the previous actions are successful:
190+
191+
```
192+
.target(
193+
name: "MyTarget",
194+
...
195+
swiftSettings: [
196+
// ... existing settings,
197+
.enableUpcomingFeature("ExistentialAny")
198+
]
199+
)
200+
...
201+
.testTarget(
202+
name: "MyTest",
203+
...
204+
swiftSettings: [
205+
// ... existing settings,
206+
.enableUpcomingFeature("ExistentialAny")
207+
]
208+
)
209+
```
210+
211+
In the "whole package" mode, every target is going to be updated to include
212+
new feature flag(s). This is supported by the same functionality as `swift package add-setting` command.
213+
214+
If it's, for some reason, impossible to add the setting the diagnostic message would suggest what to add and where i.e. `...; please add 'ExistentialAny' feature to `MyTarget` target manually`.
215+
216+
#### Impact on Interface
217+
218+
This proposal introduces a new command but does that does not interfere with existing commands. It follows the same pattern as `swift build` and `swift test` in a consistent manner.
219+
220+
## Source compatibility
221+
222+
This proposal does not affect language rules.
223+
The described changes to the API surface are source-compatible.
224+
225+
## ABI compatibility
226+
227+
This proposal does not affect binary compatibility or binary interfaces.
228+
229+
## Implications on adoption
230+
231+
Entering or exiting migration mode can affect behavior and is therefore a
232+
potentially source-breaking action.
233+
234+
## Future directions
235+
236+
### Producing source incompatible fix-its
237+
238+
For some features, a source change that alters the semantics of
239+
the program is a more desirable approach to addressing an error that comes
240+
from enabling the feature.
241+
For example, programmers might want to replace cases of `any P` with `some P`.
242+
Migration tooling could support the option to produce source incompatible
243+
fix-its in cases where the compiler can detect that a different behavior might
244+
be more beneficial.
245+
246+
### Applications beyond mechanical migration
247+
248+
The concept of migration mode could be extrapolated to additive features, such
249+
as [typed `throws`][SE-0413] or [opaque parameter types][SE-0341], by providing
250+
actionable adoption tips.
251+
Additive features are hard-enabled and become an integral part of the language
252+
as soon as they ship.
253+
Many recent additive features are already integrated into the Swift feature
254+
model, and their metadata is kept around either to support
255+
[feature availability checks][SE-0362-feature-detection] in conditional
256+
compilation blocks or because they started off as experimental features.
257+
258+
Another feasible extension of migration mode is promotion of best practices.
259+
260+
### Augmented diagnostic metadata
261+
262+
The current serialization format for diagnostics does not include information
263+
about diagnostic groups or whether a particular fix-it preserves semantics.
264+
There are several reasons why this data can be valuable for users, and why it
265+
is essential for future tools built around migration mode:
266+
* The diagnostic group name can be used to, well, group diagnostics, as well as
267+
to communicate relationships between diagnostics and features and filter out
268+
relevant diagnostics.
269+
This can prove especially handy when multiple features are simultaneously
270+
enabled in migration mode, or when similar diagnostic messages are caused by
271+
distinct features.
272+
* Exposing the purpose of a fix-it can help developers make quicker decisions
273+
when offered multiple fix-its.
274+
Furthermore, tools can take advantage of this information by favoring and
275+
auto-applying source-compatible fix-its.
276+
277+
## Alternatives considered
278+
279+
### A distinct `-migrate` option
280+
281+
This direction has a questionably balanced set of advantanges and downsides.
282+
On one hand, it would provide an adequate foundation for invoking migration
283+
for a language mode in addition to individual features.
284+
On the other hand, an independent option is less discoverable, has a steeper
285+
learning curve, and makes the necessary relationships between it and the
286+
existing `-enable-*-feature` options harder to infer.
287+
Perhaps more notably, a bespoke option by itself would not scale to any future
288+
modes, setting what might be an unfortunate example for further decentralizion
289+
of language feature control.
290+
291+
### API for package manifests
292+
293+
The decision around surfacing migration mode in the `PackageDescription`
294+
library depends on whether there is a concensus on the value of enabling it as
295+
a persistent setting as opposed to an automated procedure in the long run.
296+
297+
Here is how an API change could look like for the proposed solution:
298+
299+
```swift
300+
+extension SwiftSetting {
301+
+ @available(_PackageDescription, introduced: 6.2)
302+
+ public enum SwiftFeatureMode {
303+
+ case migrate
304+
+ case on
305+
+ }
306+
+}
307+
```
308+
```diff
309+
public static func enableUpcomingFeature(
310+
_ name: String,
311+
+ mode: SwiftFeatureMode = .on,
312+
_ condition: BuildSettingCondition? = nil
313+
) -> SwiftSetting
314+
315+
public static func enableExperimentalFeature(
316+
_ name: String,
317+
+ mode: SwiftFeatureMode = .on,
318+
_ condition: BuildSettingCondition? = nil
319+
) -> SwiftSetting
320+
```
321+
322+
It can be argued that both Swift modules and the volume of changes required for
323+
migration can be large enough to justify spreading the review over several
324+
sessions, especially if migration mode gains support for parallel
325+
[source-incompatible fix-its][#producing-source-incompatible-fix-its].
326+
However, we also expect higher-level migration tooling to allow for
327+
incremental progress.
328+
329+
### Naming
330+
331+
The next candidates in line per discussions are ***adopt***, ***audit***,
332+
***stage***, and ***preview***, respectively.
333+
* ***preview*** and ***stage*** can both be understood as to report on the
334+
impact of a change, but are less commonly used in the sense of code
335+
migration.
336+
* ***audit*** best denotes a recurrent action in this context, which we believe
337+
is more characteristic of the static analysis domain, such as enforcing a set
338+
of custom compile-time rules on code.
339+
* An important reservation about ***adoption*** of source-breaking features is
340+
that it comprises both code migration and integration.
341+
It may be more prudent to save this term for a future add-on mode that,
342+
unlike migration mode, implies that the feature is enabled, and can be invoked
343+
in any language mode to aid developers in making better use of new behaviors
344+
or rules.
345+
To illustrate, this mode could appropriately suggest switching from `any P`
346+
to `some P` for `ExistentialAny`.
347+
348+
### `swift package migrate` vs. `swift migrate`
349+
350+
Rather than have migrate as a subcommand (ie. `swift package migrate`), another option is add another top level command, ie. `swift migrate`.
351+
352+
As the command applies to the current package, we feel a `swift package` sub-command fits better than a new top-level command. This also aligns with the recently added package refactorings (eg. `add-target`).
353+
354+
## Acknowledgements
355+
356+
This proposal was inspired by documents prepared by [Allan Shortlidge] and
357+
[Holly Borla].
358+
Special thanks to Holly for her guidance throughout the draft stage.
359+
360+
<!-- Links -------------------------------------------------------------------->
361+
362+
[Holly Borla]: https://github.com/hborla
363+
[Allan Shortlidge]: https://github.com/tshortli
364+
365+
[SE-0192]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0192-non-exhaustive-enums.md
366+
[SE-0274]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0274-magic-file.md
367+
[SE-0286]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0286-forward-scan-trailing-closures.md
368+
[SE-0296]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0296-async-await.md
369+
[SE-0335]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0335-existential-any.md
370+
[SE-0337]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md
371+
[SE-0341]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0341-opaque-parameters.md
372+
[SE-0352]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0352-implicit-open-existentials.md
373+
[SE-0354]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0354-regex-literals.md
374+
[SE-0362]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md
375+
[SE-0362-feature-detection]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md#feature-detection-in-source-code
376+
[SE-0383]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0383-deprecate-uiapplicationmain-and-nsapplicationmain.md
377+
[SE-0401]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0401-remove-property-wrapper-isolation.md
378+
[SE-0401-acceptance]: https://forums.swift.org/t/accepted-with-modifications-se-0401-remove-actor-isolation-inference-caused-by-property-wrappers/66241
379+
[SE-0409]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0409-access-level-on-imports.md
380+
[SE-0411]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0411-isolated-default-values.md
381+
[SE-0413]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md
382+
[SE-0412]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0412-strict-concurrency-for-global-variables.md
383+
[SE-0418]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0418-inferring-sendable-for-methods.md
384+
[SE-0423]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0423-dynamic-actor-isolation.md
385+
[SE-0434]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0434-global-actor-isolated-types-usability.md
386+
[SE-0444]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
387+
[async-inherit-isolation-pitch]: https://forums.swift.org/t/pitch-inherit-isolation-by-default-for-async-functions/74862

0 commit comments

Comments
 (0)