Skip to content

Commit d2ca260

Browse files
authored
Add a chapter on editions. (#1835)
1 parent 44b2f27 commit d2ca260

File tree

3 files changed

+343
-0
lines changed

3 files changed

+343
-0
lines changed

src/SUMMARY.md

+4
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@
184184
- [Sanitizers Support](./sanitizers.md)
185185
- [Debugging support in the Rust compiler](./debugging-support-in-rustc.md)
186186

187+
# General Guides
188+
189+
- [Editions](guides/editions.md)
190+
187191
---
188192

189193
[Appendix A: Background topics](./appendix/background.md)

src/diagnostics.md

+3
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,8 @@ declare_lint! {
689689
This makes the `ANONYMOUS_PARAMETERS` lint allow-by-default in the 2015 edition
690690
but warn-by-default in the 2018 edition.
691691

692+
See [Edition-specific lints](../guides/editions.md#edition-specific-lints) for more information.
693+
692694
### Feature-gated lints
693695

694696
Lints belonging to a feature should only be usable if the feature is enabled in the
@@ -720,6 +722,7 @@ meaning that rustc exclusively exposes to users as "future incompatible".
720722
meaning in an upcoming *edition*. These are often called "edition lints" and can be
721723
typically seen in the various "edition compatibility" lint groups (e.g., `rust_2021_compatibility`)
722724
that are used to lint against code that will break if the user updates the crate's edition.
725+
See [migration lints](guides/editions.md#migration-lints) for more details.
723726

724727
A future-incompatible lint should be declared with the `@future_incompatible`
725728
additional "field":

src/guides/editions.md

+336
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
# Editions
2+
3+
<!-- toc -->
4+
5+
This chapter gives an overview of how Edition support works in rustc.
6+
This assumes that you are familiar with what Editions are (see the [Edition Guide]).
7+
8+
[Edition Guide]: https://doc.rust-lang.org/edition-guide/
9+
10+
## Edition definition
11+
12+
The `--edition` CLI flag specifies the edition to use for a crate.
13+
This can be accessed from [`Session::edition`].
14+
There are convenience functions like [`Session::at_least_rust_2021`] for checking the crate's
15+
edition, though you should be careful about whether you check the global session or the span, see
16+
[Edition hygiene] below.
17+
18+
As an alternative to the `at_least_rust_20xx` convenience methods, the [`Edition`] type also
19+
supports comparisons for doing range checks, such as `span.edition() >= Edition::Edition2021`.
20+
21+
[`Session::edition`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_session/struct.Session.html#method.edition
22+
[`Session::at_least_rust_2021`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_session/struct.Session.html#method.at_least_rust_2021
23+
[`Edition`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_span/edition/enum.Edition.html
24+
25+
### Adding a new edition
26+
27+
Adding a new edition mainly involves adding a variant to the [`Edition`] enum and then fixing
28+
everything that is broken. See [#94461](https://github.com/rust-lang/rust/pull/94461) for an
29+
example.
30+
31+
### Features and Edition stability
32+
33+
The [`Edition`] enum defines whether or not an edition is stable.
34+
If it is not stable, then the `-Zunstable-options` CLI option must be passed to enable it.
35+
36+
When adding a new feature, there are two options you can choose for how to handle stability with a
37+
future edition:
38+
39+
- Just check the edition of the span like `span.at_least_rust_20xx()` (see [Edition hygiene]) or the
40+
[`Session::edition`]. This will implicitly depend on the stability of the edition itself to
41+
indicate that your feature is available.
42+
- Place your new behavior behind a [feature gate].
43+
44+
It may be sufficient to only check the current edition for relatively simple changes.
45+
However, for larger language changes, you should consider creating a feature gate.
46+
There are several benefits to using a feature gate:
47+
48+
- A feature gate makes it easier to work on and experiment with a new feature.
49+
- It makes the intent clear when the `#![feature(…)]` attribute is used that your new feature is
50+
being enabled.
51+
- It makes testing of editions easier so that features that are not yet complete do not interfere
52+
with testing of edition-specific features that are complete and ready.
53+
- It decouples the feature from an edition, which makes it easier for the team to make a deliberate
54+
decision of whether or not a feature should be added to the next edition when the feature is
55+
ready.
56+
57+
When a feature is complete and ready, the feature gate can be removed (and the code should just
58+
check the span or `Session` edition to determine if it is enabled).
59+
60+
There are a few different options for doing feature checks:
61+
62+
- For highly experimental features, that may or may not be involved in an edition, they can
63+
implement regular feature gates like `tcx.features().my_feature`, and ignore editions for the time
64+
being.
65+
66+
- For experimental features that *might* be involved in an edition, they should implement gates with
67+
`tcx.features().my_feature && span.at_least_rust_20xx()`.
68+
This requires the user to still specify `#![feature(my_feature)]`, to avoid disrupting testing of
69+
other edition features which are ready and have been accepted within the edition.
70+
71+
- For experimental features that have graduated to definitely be part of an edition,
72+
they should implement gates with `tcx.features().my_feature || span.at_least_rust_20xx()`,
73+
or just remove the feature check altogether and just check `span.at_least_rust_20xx()`.
74+
75+
If you need to do the feature gating in multiple places, consider placing the check in a single
76+
function so that there will only be a single place to update. For example:
77+
78+
```rust,ignore
79+
// An example from Edition 2021 disjoint closure captures.
80+
81+
fn enable_precise_capture(tcx: TyCtxt<'_>, span: Span) -> bool {
82+
tcx.features().capture_disjoint_fields || span.rust_2021()
83+
}
84+
```
85+
86+
See [Lints and stability](#lints-and-stability) below for more information about how lints handle
87+
stability.
88+
89+
[feature gate]: ../feature-gates.md
90+
91+
## Edition parsing
92+
93+
For the most part, the lexer is edition-agnostic.
94+
Within [`StringReader`], tokens can be modified based on edition-specific behavior.
95+
For example, C-String literals like `c"foo"` are split into multiple tokens in editions before 2021.
96+
This is also where things like reserved prefixes are handled for the 2021 edition.
97+
98+
Edition-specific parsing is relatively rare. One example is `async fn` which checks the span of the
99+
token to determine if it is the 2015 edition, and emits an error in that case.
100+
This can only be done if the syntax was already invalid.
101+
102+
If you need to do edition checking in the parser, you will normally want to look at the edition of
103+
the token, see [Edition hygiene].
104+
In some rare cases you may instead need to check the global edition from [`ParseSess::edition`].
105+
106+
Most edition-specific parsing behavior is handled with [migration lints] instead of in the parser.
107+
This is appropriate when there is a *change* in syntax (as opposed to new syntax).
108+
This allows the old syntax to continue to work on previous editions.
109+
The lint then checks for the change in behavior.
110+
On older editions, the lint pass should emit the migration lint to help with migrating to new
111+
editions.
112+
On newer editions, your code should emit a hard error with `emit_err` instead.
113+
For example, the deprecated `start...end` pattern syntax emits the
114+
[`ellipsis_inclusive_range_patterns`] lint on editions before 2021, and in 2021 is an hard error via
115+
the `emit_err` method.
116+
117+
[`StringReader`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/lexer/struct.StringReader.html
118+
[`ParseSess::edition`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_session/parse/struct.ParseSess.html#structfield.edition
119+
[`ellipsis_inclusive_range_patterns`]: https://doc.rust-lang.org/nightly/rustc/lints/listing/warn-by-default.html#ellipsis-inclusive-range-patterns
120+
121+
### Keywords
122+
123+
New keywords can be introduced across an edition boundary.
124+
This is implemented by functions like [`Symbol::is_used_keyword_conditional`], which rely on the
125+
ordering of how the keywords are defined.
126+
127+
When new keywords are introduced, the [`keyword_idents`] lint should be updated so that automatic
128+
migrations can transition code that might be using the keyword as an identifier (see
129+
[`KeywordIdents`]).
130+
An alternative to consider is to implement the keyword as a weak keyword if the position it is used
131+
is sufficient to distinguish it.
132+
133+
An additional option to consider is the `k#` prefix which was introduced in [RFC 3101].
134+
This allows the use of a keyword in editions *before* the edition where the keyword is introduced.
135+
This is currently not implemented.
136+
137+
[`Symbol::is_used_keyword_conditional`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_span/symbol/struct.Symbol.html#method.is_used_keyword_conditional
138+
[`keyword_idents`]: https://doc.rust-lang.org/nightly/rustc/lints/listing/allowed-by-default.html#keyword-idents
139+
[`KeywordIdents`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/builtin/struct.KeywordIdents.html
140+
[RFC 3101]: https://rust-lang.github.io/rfcs/3101-reserved_prefixes.html
141+
142+
### Edition hygiene
143+
[edition hygiene]: #edition-hygiene
144+
145+
Spans are marked with the edition of the crate that the span came from.
146+
See [Macro hygiene] in the Edition Guide for a user-centric description of what this means.
147+
148+
You should normally use the edition from the token span instead of looking at the global `Session`
149+
edition.
150+
For example, use `span.edition().at_least_rust_2021()` instead of `sess.at_least_rust_2021()`.
151+
This helps ensure that macros behave correctly when used across crates.
152+
153+
[Macro hygiene]: https://doc.rust-lang.org/nightly/edition-guide/editions/advanced-migrations.html#macro-hygiene
154+
155+
## Lints
156+
157+
Lints support a few different options for interacting with editions.
158+
Lints can be *future incompatible edition migration lints*, which are used to support
159+
[migrations][migration lints] to newer editions.
160+
Alternatively, lints can be [edition-specific](#edition-specific-lints), where they change their
161+
default level starting in a specific edition.
162+
163+
### Migration lints
164+
[migration lints]: #migration-lints
165+
[migration lint]: #migration-lints
166+
167+
*Migration lints* are used to migrate projects from one edition to the next.
168+
They are implemented with a `MachineApplicable` [suggestion](../diagnostics.md#suggestions) which
169+
will rewrite code so that it will **successfully compile in both the previous and the next
170+
edition**.
171+
For example, the [`keyword_idents`] lint will take identifiers that conflict with a new keyword to
172+
use the raw identifier syntax to avoid the conflict (for example changing `async` to `r#async`).
173+
174+
Migration lints must be declared with the [`FutureIncompatibilityReason::EditionError`] or
175+
[`FutureIncompatibilityReason::EditionSemanticsChange`] [future-incompatible
176+
option](../diagnostics.md#future-incompatible-lints) in the lint declaration:
177+
178+
```rust,ignore
179+
declare_lint! {
180+
pub KEYWORD_IDENTS,
181+
Allow,
182+
"detects edition keywords being used as an identifier",
183+
@future_incompatible = FutureIncompatibleInfo {
184+
reason: FutureIncompatibilityReason::EditionError(Edition::Edition2018),
185+
reference: "issue #49716 <https://github.com/rust-lang/rust/issues/49716>",
186+
};
187+
}
188+
```
189+
190+
When declared like this, the lint is automatically added to the appropriate
191+
`rust-20xx-compatibility` lint group.
192+
When a user runs `cargo fix --edition`, cargo will pass the `--force-warn rust-20xx-compatibility`
193+
flag to force all of these lints to appear during the edition migration.
194+
Cargo also passes `--cap-lints=allow` so that no other lints interfere with the edition migration.
195+
196+
Migration lints can be either `Allow` or `Warn` by default.
197+
If it is `Allow`, users usually won't see this warning unless they are doing an edition migration
198+
manually or there is a problem during the migration.
199+
Most migration lints are `Allow`.
200+
201+
If it is `Warn` by default, users on all editions will see this warning.
202+
Only use `Warn` if you think it is important for everyone to be aware of the change, and to
203+
encourage people to update their code on all editions.
204+
Beware that new warn-by-default lint that hit many projects can be very disruptive and frustrating
205+
for users.
206+
You may consider switching an `Allow` to `Warn` several years after the edition stabilizes.
207+
This will only show up for the relatively small number of stragglers who have not updated to the new
208+
edition.
209+
210+
[`keyword_idents`]: https://doc.rust-lang.org/nightly/rustc/lints/listing/allowed-by-default.html#keyword-idents
211+
[`FutureIncompatibilityReason::EditionError`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint_defs/enum.FutureIncompatibilityReason.html#variant.EditionError
212+
[`FutureIncompatibilityReason::EditionSemanticsChange`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint_defs/enum.FutureIncompatibilityReason.html#variant.EditionSemanticsChange
213+
214+
### Edition-specific lints
215+
216+
Lints can be marked so that they have a different level starting in a specific edition.
217+
In the lint declaration, use the `@edition` marker:
218+
219+
```rust,ignore
220+
declare_lint! {
221+
pub SOME_LINT_NAME,
222+
Allow,
223+
"my lint description",
224+
@edition Edition2024 => Warn;
225+
}
226+
```
227+
228+
Here, `SOME_LINT_NAME` defaults to `Allow` on all editions before 2024, and then becomes `Warn`
229+
afterwards.
230+
231+
This should generally be used sparingly, as there are other options:
232+
233+
- Small impact stylistic changes unrelated to an edition can just make the lint `Warn` on all
234+
editions. If you want people to adopt a different way to write things, then go ahead and commit to
235+
having it show up for all projects.
236+
237+
Beware that if a new warn-by-default lint hits many projects, it can be very disruptive and
238+
frustrating for users.
239+
240+
- Change the new style to be a hard error in the new edition, and use a [migration lint] to
241+
automatically convert projects to the new style. For example,
242+
[`ellipsis_inclusive_range_patterns`] is a hard error in 2021, and warns in all previous editions.
243+
244+
Beware that these cannot be added after the edition stabilizes.
245+
246+
- Migration lints can also change over time.
247+
For example, the migration lint can start out as `Allow` by default.
248+
For people performing the migration, they will automatically get updated to the new code.
249+
Then, after some years, the lint can be made to `Warn` in previous editions.
250+
251+
For example [`anonymous_parameters`] was a 2018 Edition migration lint (and a hard-error in 2018)
252+
that was `Allow` by default in previous editions.
253+
Then, three years later, it was changed to `Warn` for all previous editions, so that all users got
254+
a warning that the style was being phased out.
255+
If this was a warning from the start, it would have impacted many projects and be very disruptive.
256+
By making it part of the edition, most users eventually updated to the new edition and were
257+
handled by the migration.
258+
Switching to `Warn` only impacted a few stragglers who did not update.
259+
260+
[`ellipsis_inclusive_range_patterns`]: https://doc.rust-lang.org/nightly/rustc/lints/listing/warn-by-default.html#ellipsis-inclusive-range-patterns
261+
[`anonymous_parameters`]: https://doc.rust-lang.org/nightly/rustc/lints/listing/warn-by-default.html#anonymous-parameters
262+
263+
### Lints and stability
264+
265+
Lints can be marked as being unstable, which can be helpful when developing a new edition feature,
266+
and you want to test out a migration lint.
267+
The feature gate can be specified in the lint's declaration like this:
268+
269+
```rust,ignore
270+
declare_lint! {
271+
pub SOME_LINT_NAME,
272+
Allow,
273+
"my cool lint",
274+
@feature_gate = sym::my_feature_name;
275+
}
276+
```
277+
278+
Then, the lint will only fire if the user has the appropriate `#![feature(my_feature_name)]`.
279+
Just beware that when it comes time to do crater runs testing the migration that the feature gate
280+
will need to be removed.
281+
282+
Alternatively, you can implement an allow-by-default [migration lint] for an upcoming unstable
283+
edition without a feature gate.
284+
Although users may technically be able to enable the lint before the edition is stabilized, most
285+
will not notice the new lint exists, and it should not disrupt anything or cause any breakage.
286+
287+
### Idiom lints
288+
289+
In the 2018 edition, there was a concept of "idiom lints" under the `rust-2018-idioms` lint group.
290+
The concept was to have new idiomatic styles under a different lint group separate from the forced
291+
migrations under the `rust-2018-compatibility` lint group, giving some flexibility as to how people
292+
opt-in to certain edition changes.
293+
294+
Overall this approach did not seem to work very well,
295+
and it is unlikely that we will use the idiom groups in the future.
296+
297+
## Standard library changes
298+
299+
### Preludes
300+
301+
Each edition comes with a specific prelude of the standard library.
302+
These are implemented as regular modules in [`core::prelude`] and [`std::prelude`].
303+
New items can be added to the prelude, just beware that this can conflict with user's pre-existing
304+
code.
305+
Usually a [migration lint] should be used to migrate existing code to avoid the conflict.
306+
For example, [`rust_2021_prelude_collisions`] is used to handle the collisions with the new traits
307+
in 2021.
308+
309+
[`core::prelude`]: https://doc.rust-lang.org/core/prelude/index.html
310+
[`std::prelude`]: https://doc.rust-lang.org/std/prelude/index.html
311+
[`rust_2021_prelude_collisions`]: https://doc.rust-lang.org/nightly/rustc/lints/listing/allowed-by-default.html#rust-2021-prelude-collisions
312+
313+
### Customized language behavior
314+
315+
Usually it is not possible to make breaking changes to the standard library.
316+
In some rare cases, the teams may decide that the behavior change is important enough to break this
317+
rule.
318+
The downside is that this requires special handling in the compiler to be able to distinguish when
319+
the old and new signatures or behaviors should be used.
320+
321+
One example is the change in method resolution for [`into_iter()` of arrays][into-iter].
322+
This was implemented with the `#[rustc_skip_array_during_method_dispatch]` attribute on the
323+
`IntoIterator` trait which then tells the compiler to consider an alternate trait resolution choice
324+
based on the edition.
325+
326+
Another example is the [`panic!` macro changes][panic-macro].
327+
This required defining multiple panic macros, and having the built-in panic macro implementation
328+
determine the appropriate way to expand it.
329+
This also included the [`non_fmt_panics`] [migration lint] to adjust old code to the new form, which
330+
required the `rustc_diagnostic_item` attribute to detect the usage of the panic macro.
331+
332+
In general it is recommended to avoid these special cases except for very high value situations.
333+
334+
[into-iter]: https://doc.rust-lang.org/nightly/edition-guide/rust-2021/IntoIterator-for-arrays.html
335+
[panic-macro]: https://doc.rust-lang.org/nightly/edition-guide/rust-2021/panic-macro-consistency.html
336+
[`non_fmt_panics`]: https://doc.rust-lang.org/nightly/rustc/lints/listing/warn-by-default.html#non-fmt-panics

0 commit comments

Comments
 (0)