The #[ignore] attribute can now be applied to fields.
Its purpose is to tell derive macros to skip the field when generating code.
For the above struct User, derives PartialEq and Hash will ignore the name and age fileds.
Code like this is generated:
Motivation
It's common to want to exclude one or more fields when deriving traits such as Debug or Deserialize.
To do this, you currently need to completely abandon the derive and instead implement the trait manually.
Manually implementing the trait is much more error-prone than letting the derive do it for you. For example, when a new field is added, it's possible to forget to change all of your implementations.
This is why it's idiomatic to instead derive traits when it's possible to do so. But this deriving isn't possible if you need to ignore a field.
Common use-cases of ignoring fields when implementing traits include:
-
Types that are high-level wrappers around other types, which may include some additional metadata but ultimately delegate most trait implementations to the inner type. An example is an identifier in programming languages:
/// Identifier for a variable, function or trait -
Minor improvements such as skipping irrelevant fields in a
Debugderive (like fields of typePhantomData) derive will become easy enough for people to just do. Currently, the effort required to maintain a manual implementation is too high - so people just don't bother with it in most cases. -
Security: Data with sensitive fields like AWS s3
access_key_idandsecret_access_keythat must be skipped forDebugimplementations is forced to be implemented manually - leading to a significant increase in boilerplate
Guide-level explanation
When using #[derive], you can apply #[ignore] to fields:
The #[ignore] receives paths to a subset of the derive macros applied to the item.
It is invalid for #[ignore] to receive a derive that isn't applied to the current item:
In the above example, Foo derives Clone but not PartialEq - so passing PartialEq to ignore is disallowed.
Variants
You can also apply #[ignore] to enum variants, too:
From the perspective of a derive macro
When a derive macro such as #[derive(std::hash::Hash)] is applied to an item like a struct:
-
Any
#[ignore]attributes that mention the derive itself, in this casestd::hash::Hash, will be a part of theTokenStreamthat thederivemacro receives - with the list of derives removed. The derive macro has no idea what other derives ignore this field, it just knows that it should ignore it.Example:
std::hash::Hashwill see#[ignore] field: ()when the input contains#[ignore(std::hash::Hash, PartialEq)] field: () -
If the
#[ignore]attribute does not mention the derive, then the attribute is removed completely from the macro's inputTokenStream. The derive macro doesn't know that other derives ignore this field.Example:
PartialEqwill see justfield: ()when the input contains#[ignore(std::hash::Hash)] field: ()
Example
In the below example:
PartialEqwill ignore fieldsbarandbazstd::hash::Hashwill ignore fieldsfooandbaz
std::hash::Hash receives this TokenStream:
Explanation:
- The
#[ignore]applied tofoocontainsstd::hash::Hash - The
#[ignore]applied tobardoes NOT containstd::hash::Hash - The
#[ignore]applied tobazcontainsstd::hash::Hash - There is no
#[ignore]applied toquux
The #[ignore] attribute is included for foo and baz in std::hash::Hash's input TokenStream
Then it's up to the std::hash::Hash macro on how exactly it wants to use the #[ignore] attribute.
- In the common case, it will exclude
fooandbazfrom the generatedstd::hash::Hashimpl std::hash::Hashis allowed to ignore existence of the attribute.
Standard library macros that support #[ignore]
The following standard library traits support #[ignore]:
PartialEqPartialOrdOrdHashDebug
How this impacts code maintainability
Given a Var like this:
You want to implement:
PartialEqandHashsuch that onlynsandsymfields are hashes and comparedDebugsuch that it skips the_phantomfield
Without #[ignore] on fields
you'd need to implement those 3 traits manually:
use Debug;
use Hash;
Notes:
- It is logically incorrect for
HashandPartialEqimplementations to differ, so you must remember to keep them in sync ifVarchanges - You must remember to update the string names of the
Debugif you ever rename the fields orVaritself
With #[ignore]
Notes:
- Multiple
#[ignore]attributes can apply to the same field, which is the same as writing each argument toignorein a single attribute. - Unlike the manual implementations, the code will generate trait implementations that require the type parameter
Tto be bounded by the implementing trait, for exampleimpl<T: Debug> Debug for Var<T>. Explained in the next section.
Reference-level explanation
The #[ignore] attribute now also applies to:
- Fields of
structs - Fields of enum variarnts
- Fields of
unions - Enum variants
Notes:
- Fields can be either named or unnamed.
- When applied to fields,
#[ignore]takes a list ofSimplePaths separated by comma, with an optional trailing comma, e.g.#[ignore(Foo, Bar,)]. - The list is allowed to be empty:
#[ignore()]but it must exist. Just#[ignore]without the()is not allowed. - Multiple
#[ignore]attributes on a field are allowed to appear in a row, in which case their list of paths merges into a single#[ignore]. The derive macro still just receives a single#[ignore]if it's present in the list of paths, no matter how many#[ignore]attributes were used on the field.
] );
union Union
Name resolution
Paths supplied to #[ignore] must resolve to a derive macro applied to the current item.
use Hash as RenamedHash;
The above works, because RenamedHash is std::hash::Hash.
Unknown derive
A path supplied to ignore that does not apply to the current derive is disallowed. For example:
Duplicate derive
The list of paths passed to ignore is not allowed to contain more than 1 of the same path:
Inside the macro
The #[ignore] attribute(s) for a particular derive macro Foo applied to the current item will do one of 2 things
once they become an input TokenStream to the derive macro Foo:
- If the list of paths includes the derive macro
Fooitself, then an#[ignore]attribute without any arguments is applied to the field - If the list of paths to
ignoredoes not name the the derive macro, then theignoreattribute is fully removed.
Declaring support
Derive macros must explicitly declare that they support the ignore attribute:
If the derive macro doesn't support ignore, then any usage of #[ignore] on fields will yield an error.
#[ignore] without parentheses, and #[ignore = "some string"] will compile and do nothing (with a warn-by-default or deny-by-default lint),
even if no support for the ignore attribute is declared by the derive macro. This is discussed further in the "Rationale and alternatives" section.
Spans
Because input to the macro is different from what it is in the source code, we have to talk about
the Span that the derive macros see. Derive macros can use this span for various purposes.
For example, since this RFC does not make an attempt to fit every possible use-case of ignoring fields, some derive macros
might require additional information about how to ignore the field. They can use this span to create an error and re-direct
users to their custom #[foo(ignore(...))] that might take arguments for example.
The span
Given the following struct:
Each of std::hash::Hash, PartialEq, and Debug will receive the following input, except that span of the #[ignore] will differ:
The span of #[ignore] attribute received by derive macro Debug will be the same as the path Debug as originally passed to the
#[ignore(Debug)] attribute. Span is indicated with ^^^^ in the following examples:
-
std::hash::Hash: -
PartialEq: -
Debug:
New lints
This RFC additionally proposes to add 2 new deny-by-default lints:
-
Types that implement or derive
Eqwith fields that ignore just one ofHashorPartialEqissue a lint, because typesk1andk2implementingEqandHashare expected to follow the property:k1 == k2 -> hash(k1) == hash(k2)Violating this property is a logic error, so it would be incorrect to
#[ignore]only 1 of those traits.If the type manually implements either
HashorPartialEq, the lint does not fire -
Types with fields that ignore just one, or just two of
PartialEq,PartialOrdandOrdissue a lint, because it is logically incorrect for the implementations to differ. See the documentation for details.Essentially, fields must either ignore all of them
#[ignore(PartialEq, PartialOrd, Ord)], or none.If the type manually implements any of
PartialEq,PartialOrd, orOrdthe lint does not fire
In some circumstances mentioned above, lints won't fire because checking if the given properties are upheld will require checking body of the trait implementations and scanning for used/unused fields. While this could theoretically be accomplished with some heuristics, this RFC does not require that to happen
Standard library derives supporting the ignore attribute
PartialEqPartialOrdOrdDebugHash
#[derive(PartialEq)] does not implement StructuralPartialEq if any fields are ignored
By default, #[derive(PartialEq)] automatically implements StructuralPartialEq,
and the invariant automatically upheld is the following:
interpreting the value of the constant as a pattern is equivalent to calling PartialEq
Essentially, given any type Foo implementing PartialEq, both A and B must be identical:
const FOO: Foo = Foo ;
// A
match foo
// B
match foo
But if any field is #[ignore(PartialEq)]d, then the property would be violated:
const FOO: Foo = Foo ;
// Then this
match foo
// Is actually this:
match foo
// The above is NOT equivalent to this:
match foo
Hence any type deriving PartialEq with fields that are marked #[ignore(PartialEq)] will not implement StructuralPartialEq automatically
For the default derives, type parameter bounds are not affected by ignore
Even if a type parameter is not used in any of the non-ignored fields, like here:
Code will still be generated that requires T to implement Debug:
It would be possible to implement this, but it would be a SemVer hazard.
The field value is private. One would rightfully expect that making changes to it should not affect the public API.
The crate author might remove that ignore attribute, expecting it to be okay:
#[derive(Debug)]
pub struct Value<T> {
- #[ignore(Debug)]
value: Option<T>,
}
This will be a breaking change, because now, Value<T> only implements Debug if T does.
When the ignore was still there, Value<T> would always implement Debug.
This behaviour is similar to how here:
The generated code requires T to implement Clone, even though it doesn't need to (because Rc is always Clone):
The macro could generate the following code instead:
But this will commit to Value<T> being Clone even when T is not. So if the library author then removes the
Rc wrapper to instead own that T, this will be a breaking change:
Drawbacks
It overloads ignore to mean 2 different things, as it currently has 1 meaning: functions marked with #[test] will be ignored from the test suite
Rationale and alternatives
There was an attempt to implement this feature with the #[skip] attribute before.
However, this lead to unacceptable breaking changes:
To give an update (at long last), the crater report did confirm my suspicion. We have 4 confirmed root regressions: [1], [2], [3], [4], [4.1], [4.2].
Considering the breakage would compromise Rust's stability guarantees, a different design is required:
Given the crater results showing compatibility hazards, it sounds like this is going to need a design and lang RFC for an approach that avoids those.
This is that RFC. It proposes a design that avoids any breaking changes, by re-using the conveniently named ignore attribute for a new purpose.
What if someone already has a custom attribute macro named ignore?
Impossible.
The #[skip] built-in attribute could not be used because even the feature gate would break existing code from compiling.
In this RFC, the #[ignore] attribute avoids that.
This attribute is already built-in, and people cannot apply an attribute macro named ignore as that would be ambiguous:
use derive as ignore;
The above yields an ambiguity error:
error[E0659]: `ignore` is ambiguous
--> src/main.rs:4:7
|
4 | #[ignore]
| ^^^^^^ ambiguous name
|
= note: ambiguous because of a name conflict with a builtin attribute
= note: `ignore` could refer to a built-in attribute
note: `ignore` could also refer to the attribute macro imported here
--> src/main.rs:1:5
|
1 | use derive as ignore;
| ^^^^^^^^^^^^^^^^
= help: use `crate::ignore` to refer to this attribute macro unambiguously
You must use the attribute as #[ignore()] where inside of the parentheses we can specify a list of paths.
Because today #[ignore] is already valid on fields:
warning: `#[ignore]` only has an effect on functions
--> src/main.rs:2:5
|
2 | #[ignore]
| ^^^^^^^^^
|
= note: `#[warn(unused_attributes)]` on by default
It could be possible to implement #[ignore] (without a list of paths) on fields over an edition, maybe. This is discussed in the "Future Possibilities" section.
By explicitly requiring the list of arguments with parentheses, we can just feature-gate that syntax - which is currently invalid with a deny-by-default lint:
error: valid forms for the attribute are `#[ignore]` and `#[ignore = "reason"]`
--> src/main.rs:2:5
|
2 | #[ignore()]
| ^^^^^^^^^^^
|
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #57571 <https://github.com/rust-lang/rust/issues/57571>
= note: `#[deny(ill_formed_attribute_input)]` on by default
Once this RFC is implemented, the above deny-by-default lint is promoted into a hard error on the stable channel. This should be acceptable as it's unlikely someone uses this invalid syntax today - which doesn't do anything. Even if so, the lint clearly states that it will become an error in a future release. The lint was implemented in January 2019. See the tracking issue. It has been almost 6 years since it became deny-by-default. It should be fine to promote it into a hard error as that's what this RFC would require for the feature-gate
#[ignore] (without parentheses) on fields already compiles. Would this be a breaking change?
You can currently apply #[ignore] to fields at the moment in 2 ways, which leads to a warn-by-default unused_attributes lint:
#[ignore]without parentheses#[ignore = "some string"]
For example:
The above gives warnings:
warning: `#[ignore]` only has an effect on functions
--> src/main.rs:2:5
|
2 | #[ignore]
| ^^^^^^^^^
|
= note: `#[warn(unused_attributes)]` on by default
warning: `#[ignore]` only has an effect on functions
--> src/main.rs:4:5
|
4 | #[ignore = "for some reason"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Both attributes do nothing, but are syntactically valid. Under this RFC, they will continue to compile with warnings, probably with a changed error message to alert the user that they do absolutely nothing.
Upgrading these warnings into a deny-by-default future incompatibility lint is discussed in the "Future Possibilities" section.
Why not another name?
As seen with the #[skip] attribute attempt, it is likely to lead to breakages when we try to create a new built-in attribute for this.
The ignore keyword is very convenient for us, because it lets us implement this feature using an understandable keyword
while not compromising on the stability guarantees.
How about #[debug(ignore)] or something like that?
That's how the ecosystem works at the moment. Crates such as serde, clap each have their own attributes for ignoring a field: #[serde(skip)].
There is also the crate derivative which implements the ignore functionality for standard library traits:
There are numerous disadvantages to not having a single attribute in the standard library for skipping fields when deriving:
-
Blocks standard library derives like
Hash,Ord,PartialOrd,Eq,PartialEqfrom ignoring fields, because if adding the 1skipattribute lead to so much breakage imagine if we added 5 new attributes -
People have to learn each crate's own way of skipping fields, if it exists at all. With
#[ignore]on fields, the user can instead use the same syntax for every derive macro.If they try adding a derive macro to the the list of paths in the
ignoreattribute, the user will be notified by the compiler if the derive macro doesn't support theignoreattribute - as it is explicitly opt-in inattributes(ignore) -
Multiple
#[foo(ignore)],#[bar(ignore)]attributes each take up a separate line. A single#[ignore(Foo, Bar)]can instead nicely fit on a single line. -
Compile times. If we have a separate crate re-implement the standard library
Hashderives and others, then the compile times will be much worse since they'll require additional dependencies likesynand miss out on the optimizations that can be gained with a derive macro that is included in the standard library
Other benefits of ignore attribute include:
- Standardizes syntax and makes reading Rust code more predictable.
- Allows language servers like rust-analyzer to provide suggestions for the list of paths an
ignoreattribute would accept, making this functionality more discoverable - Similar to how multiple
#[derive]s are merged into a single#[derive]by rustfmt, multiple#[ignore]attributes could be merged into a single#[ignore]attribute
Why disallow more than 1 of the same path?
It is an error to pass 2 paths to the ignore attribute that resolve to the same item because:
- It doesn't do anything, and is likely a mistake on the user's part
- If we ever want to allow duplicate paths, we can always relax the restriction. But adding the restriction will not be possible.
What is the impact of not doing this?
- Less standardization in the ecosystem. Each crate can have its own way to do things.
- People are more likely to write buggy or incorrect code because they forget to update manual implementations of traits like
PartialEqthat only exist because they needed to exclude a field - A lot of boilerplate that is usually taken care of by derive macros
Prior art
Several crates in the Rust ecosystem currently support similar functionality.
#[serde(skip)]#[clap(skip)]derive_more::Debugis a more customizable version of thestd::Debugderive and allows skipping individual fields with#[debug(skip)]
Unresolved questions
None
Future possibilities
#[ignore] without a list of paths is visible to all macros
It could be possible to allow just #[ignore] on fields without a list of paths -
which would make #[ignore] visible to every macro. For example, make this:
Equivalent to the following:
If desired, such a change will be possible to make in the future, but it is not part of this RFC because the first code block already compiles - it would be a breaking change.
It's also not clear whether we'd want #[ignore] to work this way at all, so let's leave it up to a future
RFC to decide if giving #[ignore] (without parentheses) on fields meaning would be worth it.
Promote #[ignore] on fields (without parentheses) and #[ignore = "reason"] on fields into a deny-by-default lint
Whilst it's not currently clear whether we want #[ignore] (without parentheses) on fields to actually do something,
we could upgrade the 2 currently useless forms of #[ignore] (without parentheses) on fields and #[ignore = "reason"] on fields into a
deny-by-default future incompatibility lint - just to be safe.
This lint is not part of the RFC, and can be discussed separately.
ignore with arguments
It's possible that derive macros might find it useful to know how they should be ignored.
We could allow passing arguments to paths in ignore:
Which would give the following input to MyTrait:
This could be backward-compatible to add, so this is left for a future RFC to propose.
However it's worth noting that there are several disadvantages with this:
-
It could encourage misuse of the
ignoreattribute, using the arguments as a sort of "default" value, when it would be clearer to use default field values -
Overloads meaning of the attribute, it is not necessarily always about "ignoring" any more, rather adding a condition to serializiation
-
Makes it harder to read which derives are ignored. It's more reasonable to have a flat list of all ignored derives, and if there is any metadata about ignoring, derives can use their own helper attributes for that. Prefer:
Over stuffing all information in a single attribute:
-
None of the derives in the standard library would use this feature, and few crates would find it useful where default field values don't solve the issue. We would be supporting a mostly niche use-case