Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arbitrary self types v2: shadowing semver break. #15117

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions src/doc/src/reference/semver.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ considered incompatible.
* [Minor: generalizing a function to use generics (supporting original type)](#fn-generalize-compatible)
* [Major: generalizing a function to use generics with type mismatch](#fn-generalize-mismatch)
* [Minor: making an `unsafe` function safe](#fn-unsafe-safe)
* [Major: adding a potentially shadowing method](#fn-add-potentially-shadowing-method)
* Attributes
* [Major: switching from `no_std` support to requiring `std`](#attr-no-std-to-std)
* [Major: adding `non_exhaustive` to an existing enum, variant, or struct with no private fields](#attr-adding-non-exhaustive)
Expand Down Expand Up @@ -1883,6 +1884,136 @@ Making a previously `unsafe` associated function or method on structs / enums
safe is also a minor change, while the same is not true for associated
function on traits (see [any change to trait item signatures](#trait-item-signature)).

### Major: add a potentially shadowing method {#fn-add-potentially-shadowing-method}

If you have a type which implements `Deref<Target=T>`, you must not add methods
which may "shadow" methods in `T`. This can lead to unexpected changes in
program behavior.
Comment on lines +1889 to +1891
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"must not" is too strong.

This is intentionally done and relied on in a number of cases.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you agree with the statement if I say:

If you have a type which implements Deref<Target=T> for an arbitrary user-controlled T, you must not add methods which may "shadow" methods in T.

I'm quite keen to give a precise hard rule because it will make it easier for the cargo semver folks, if nothing else.

If you still think that's not a true statement, could you give examples of where this is intentionally done and is OK? Some of these cases will turn into hard errors when/if rust-lang/rust#135881 lands.

Copy link
Member

@obi1kenobi obi1kenobi Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the precision in the rule — indeed very helpful.

For cargo-semver-checks purposes, the use of "must not" vs a different verb doesn't matter too much. We'll implement the same lint either way, since the goal of the tool is to notify maintainers that something risky / worth a look has happened and not to tie their hands about it. There are legitimate situations where users may ship known-breaking changes in non-major releases, and that's fine. We look to prevent unknown-breaking changes from happening.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@obi1kenobi @workingjubilee I'd appreciate a little more advice here about how to resolve this concern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with whatever @adetaylor and @workingjubilee decide on. I thoroughly trust your judgment.

Copy link
Member

@obi1kenobi obi1kenobi Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a possible path forward, in an effort to save everyone else's valuable time:

Suggested change
If you have a type which implements `Deref<Target=T>`, you must not add methods
which may "shadow" methods in `T`. This can lead to unexpected changes in
program behavior.
If you have a type which implements `Deref<Target=T>`, adding methods to that type
which may "shadow" methods in `T` can cause breaking changes in program behavior.

This uses factual tone ("the change is breaking") and defers "must/should" level decisions to maintainers' judgment.


```rust,ignore,run-fail
// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[derive(Clone, Copy)]
pub struct MySmartPtr<T>(pub T);

impl<T> core::ops::Deref for MySmartPtr<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}

///////////////////////////////////////////////////////////
// After
#[derive(Clone, Copy)]
pub struct MySmartPtr<T>(pub T);

impl<T> core::ops::Deref for MySmartPtr<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> MySmartPtr<T> {
pub fn method(self) -> usize {
2
}
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::MySmartPtr;

struct SomeStruct;

impl SomeStruct {
fn method(&self) -> usize {
1
}
}

fn main() {
let mut ptr = MySmartPtr(SomeStruct);
assert_eq!(ptr.method(), 1);
}
```

Note that the shadowing and shadowed methods receive `self`
slightly differently: `self` and `&self`.
That's because Rust [searches for methods] first by value, then by `&`, then
by `&mut T`. Rust stops the search when it encounters a valid method, and
so methods later in this order may be shadowed by methods encountered earlier.

This is only a compatibility risk if the `Deref` target is
beyond your control. If your type implements `Deref` to another type where
you can fix the available methods, you can ensure no shadowing
occurs. An example is that `PathBuf` implements
`Deref<Target=Path>`.

For types which do implement `Deref` with an arbitrary target,
it's bad practice to add methods: add associated functions instead. This is
the pattern used by Rust's standard library smart pointer types, such as
`Box`, `Rc` and `Arc`.

Similar shadowing risks occur for a type implementing
`Receiver<Target=T>`. If you have a type which implements either
`Receiver<Target=T>` or `Deref<Target=T>` it may be used as a method receiver
by `T`'s methods. If your type then adds a method, you may shadow methods in
`T`. For instance:

```rust,ignore,skip
// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#![feature(arbitrary_self_types)]
pub struct MySmartPtr<T>(pub T);

impl<T> core::ops::Receiver for MySmartPtr<T> {
// or Deref
type Target = T;
}

///////////////////////////////////////////////////////////
// After
#![feature(arbitrary_self_types)]
pub struct MySmartPtr<T>(pub T);

impl<T> core::ops::Receiver for MySmartPtr<T> {
// or Deref
type Target = T;
}

impl<T> MySmartPtr<T> {
pub fn method(self) {}
}

///////////////////////////////////////////////////////////
// Example usage that will break.
#![feature(arbitrary_self_types)]
use updated_crate::MySmartPtr;

struct SomeStruct;

impl SomeStruct {
fn method(self: &MySmartPtr<Self>) {}
}

fn main() {
let ptr = MySmartPtr(SomeStruct);
ptr.method(); // Error: multiple applicable items in scope
}
```

When types like this are being used as method receivers, Rust endeavours to
do additional searches and present errors in simple cases, e.g. shadowing of
`&self` by `self` with inherent methods. This is better than invisible
behavior changes - but either way it's a compatibility break. Avoid adding
methods if you implement `Deref` or `Receiver` to an arbitrary target.

### Major: switching from `no_std` support to requiring `std` {#attr-no-std-to-std}

If your library specifically supports a [`no_std`] environment, it is a
Expand Down Expand Up @@ -2317,3 +2448,4 @@ document what your commitments are.
[wildcard patterns]: ../../reference/patterns.html#wildcard-pattern
[unused_unsafe]: ../../rustc/lints/listing/warn-by-default.html#unused-unsafe
[msrv-is-minor]: https://github.com/rust-lang/api-guidelines/discussions/231
[searches for methods]: ../../reference/expressions/method-call-expr.html#method-call-expressions