Skip to content
New issue

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

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

Already on GitHub? # to your account

add default_mismatches_new lint #14234

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

nyurik
Copy link
Contributor

@nyurik nyurik commented Feb 16, 2025

Implement #14075. Prevents rust-lang/rust#135977.

What it does

If a type has an auto-derived Default trait and a fn new() -> Self, this lint checks if the new() method performs custom logic rather than calling Self::default()

Why is this bad?

Users expect the new() method to be equivalent to default(), so if the Default trait is auto-derived, the new() method should not perform custom logic. Otherwise, there is a risk of different behavior between the two instantiation methods.

Example

#[derive(Default)]
struct MyStruct(i32);
impl MyStruct {
  fn new() -> Self {
    Self(42)
  }
}

Users are unlikely to notice that MyStruct::new() and MyStruct::default() would produce different results. The new() method should use auto-derived default() instead to be consistent:

#[derive(Default)]
struct MyStruct(i32);
impl MyStruct {
  fn new() -> Self {
    Self::default()
  }
}

Alternatively, if the new() method requires non-default initialization, implement a custom Default. This also allows you to mark the new() implementation as const:

struct MyStruct(i32);
impl MyStruct {
  const fn new() -> Self {
    Self(42)
  }
}
impl Default for MyStruct {
  fn default() -> Self {
    Self::new()
  }
}
  • Followed lint naming conventions
  • Added passing UI tests (including committed .stderr file)
  • cargo test passes locally
  • Executed cargo dev update_lints
  • Added lint documentation
  • Run cargo dev fmt

changelog: [default_mismatches_new]: new lint to catch fn new() -> Self method not matching the Default implementation

Notable cases in the wild

  • glob - default() and new() are actually different - likely was a bug that glob team wants to fix (per comments)
  • backtrace - new() creates cache with Vec::with_capacity(), whereas default() uses Vec::default(). Possibly a bug.
  • tracing_core uses Self(()) -- should this be ignored?
  • tracing_subscriber similarly uses Self { _p: () } for struct Identity { _p: () } -- probably better to use ::default()

TODO/TBD

  • Which cases of manual fn new() -> Self implementations are OK? (rather than delegating to Self::default())
    • In case Self is a unit type
  • given an rustc_hir::hir::ImplItem of a fn new() -> Self, get the body of that function
  • verify that the fn body simply does return Self::default(); and no other logic (in all variants like Default::default(), etc.
  • I did a minor modification to clippy_lints/src/default_constructed_unit_structs.rs while trying to understand its logic - I think we may want to have a is_unit_type(ty) utility function?
  • Handle generic types. This should be done in another PR due to complexity. See comment

@rustbot
Copy link
Collaborator

rustbot commented Feb 16, 2025

r? @llogiq

rustbot has assigned @llogiq.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Feb 16, 2025
@nyurik nyurik changed the title WIP: Implement new_without_default lint WIP: Implement default_mismatches_new lint Feb 16, 2025
@nyurik nyurik force-pushed the default_mismatches_new branch 2 times, most recently from c634158 to da55789 Compare February 18, 2025 04:25
@nyurik nyurik changed the title WIP: Implement default_mismatches_new lint Implement default_mismatches_new lint Feb 18, 2025
@nyurik nyurik marked this pull request as ready for review February 18, 2025 04:27
@nyurik nyurik force-pushed the default_mismatches_new branch from da55789 to fc5efa3 Compare February 18, 2025 04:52
@nyurik nyurik changed the title Implement default_mismatches_new lint add default_mismatches_new lint Feb 18, 2025
@samueltardieu
Copy link
Contributor

There will be a false positive on this one and the suggestion won't apply:

#[derive(Default)]
struct S<T> {
    o: Option<T>,
}

impl<T> S<T> {
    fn new() -> Self {
        S { o: None }
    }
}

@nyurik
Copy link
Contributor Author

nyurik commented Feb 18, 2025

@samueltardieu good catch! Auto-derive implements it with a where T: Default:

impl<T: Default> Default for S<T> {
    fn default() -> S<T> {
        S { o: Default::default() }
    }
}

What logic does the Default derive uses? Does it simply Default constraint to all generic parameters? The actual Option::<T>::default() exists for all T, not just those with Default.

Possible solutions

  • do not warn for generics (meh)
  • in case of generics, offer to implement Default manually and redirect to new() (also somewhat meh)
  • analyse code to check if T used in the new() already has Default constraint. Offer Self::default() solution only if T is constraint to Default, otherwise suggest to implement Default manually
  • ...?

@nyurik
Copy link
Contributor Author

nyurik commented Feb 18, 2025

For now, I restricted this lint to non-generic types. Lets do generics separately.

@samueltardieu
Copy link
Contributor

samueltardieu commented Feb 18, 2025

* analyse code to check if `T` used in the `new()` already has `Default` constraint

Constraints are additive, you would have also to consider the constraints on the enclosing impl as well.

@nyurik
Copy link
Contributor Author

nyurik commented Feb 18, 2025

@samueltardieu thx, so lets skip types with generics for now - even without generics this catches all the same cases as part of CI lint check. We can track it in a separate issue/PR.

@nyurik nyurik force-pushed the default_mismatches_new branch 2 times, most recently from 6990299 to b9ece48 Compare February 27, 2025 19:16
### What it does
If a type has an auto-derived `Default` trait and a `fn new() -> Self`,
this lint checks if the `new()` method performs custom logic rather
than simply calling the `default()` method.

### Why is this bad?
Users expect the `new()` method to be equivalent to `default()`,
so if the `Default` trait is auto-derived, the `new()` method should
not perform custom logic.  Otherwise, there is a risk of different
behavior between the two instantiation methods.

### Example
```rust
#[derive(Default)]
struct MyStruct(i32);
impl MyStruct {
  fn new() -> Self {
    Self(42)
  }
}
```

Users are unlikely to notice that `MyStruct::new()` and `MyStruct::default()` would produce
different results. The `new()` method should use auto-derived `default()` instead to be consistent:

```rust
#[derive(Default)]
struct MyStruct(i32);
impl MyStruct {
  fn new() -> Self {
    Self::default()
  }
}
```

Alternatively, if the `new()` method requires a non-default initialization, implement a custom `Default`.
This also allows you to mark the `new()` implementation as `const`:

```rust
struct MyStruct(i32);
impl MyStruct {
  const fn new() -> Self {
    Self(42)
  }
}
impl Default for MyStruct {
  fn default() -> Self {
    Self::new()
  }
}
```
@nyurik nyurik force-pushed the default_mismatches_new branch from b9ece48 to b36567a Compare March 1, 2025 07:40
@nyurik
Copy link
Contributor Author

nyurik commented Mar 1, 2025

@samueltardieu should @llogiq or someone else be assigned as the reviewer? (per your priv comment)

@samueltardieu samueltardieu requested a review from llogiq March 1, 2025 07:45
@samueltardieu
Copy link
Contributor

@samueltardieu should @llogiq or someone else be assigned as the reviewer? (per your priv comment)

This should not be necessary as the issue is assigned to him already, but just in case I've requested his review.

/// }
/// ```
///
/// Alternatively, if the `new()` method requires a non-default initialization, implement a custom `Default`.
Copy link
Contributor

Choose a reason for hiding this comment

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

This would unify the new and default implementation, losing either one or the other. I think the docs should suggest renaming new to clarify the difference.

Copy link
Contributor Author

@nyurik nyurik Mar 3, 2025

Choose a reason for hiding this comment

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

@llogiq thx for review! Could you clarify how you think the docs should change? The rustc issue that actually caused me to write this lint was that new() would set the separator to ' ', whereas the default() set it to '\0'. So unifying them should loose one or the other, and consolidate the code, but keep the original function as new()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

P.S. are there any blockers to start the FCP?

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants