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

Add new confusing_method_to_numeric_cast lint #13979

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

GuillaumeGomez
Copy link
Member

@GuillaumeGomez GuillaumeGomez commented Jan 10, 2025

Fixes #13973.

I don't think we can make fn_to_numeric_cast_any to be emitted in some special cases. Its category cannot be changed at runtime.

I think in this case, the best might be a specialized new lint so we can target exactly what we want.


changelog: Add new confusing_method_to_numeric_cast lint

@rustbot
Copy link
Collaborator

rustbot commented Jan 10, 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 Jan 10, 2025
// We get the type on which the `min`/`max` method of the `Ord` trait is implemented.
&& let [ty] = generics.as_slice()
&& let Some(ty) = ty.as_type()
// We get its name in case it's a primitive with an associated MIN/MAx constant.
Copy link
Member

Choose a reason for hiding this comment

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

typo

Suggested change
// We get its name in case it's a primitive with an associated MIN/MAx constant.
// We get its name in case it's a primitive with an associated MIN/MAX constant.

use rustc_lint::LateContext;
use rustc_middle::ty::{self, Ty};

use super::PRIMITIVE_METHOD_TO_NUMERIC_CAST;
Copy link
Member

@alex-semenyuk alex-semenyuk Jan 10, 2025

Choose a reason for hiding this comment

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

Are there primitive methods only max, min
If so might make sense to specify them at lint name

Copy link
Contributor

Choose a reason for hiding this comment

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

Or confusing_method_to_numeric_cast?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm very bad at naming so I'll take your suggestion. :3

/// ```no_run
/// let _ = u16::MAX as usize;
/// ```
#[clippy::version = "1.85.0"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: 1.86.0

@GuillaumeGomez GuillaumeGomez force-pushed the primitive_method_to_numeric_cast branch from 359e815 to 042556c Compare January 10, 2025 23:44
@GuillaumeGomez GuillaumeGomez changed the title Add new primitive_method_to_numeric_cast lint Add new confusing_method_to_numeric_cast lint Jan 10, 2025
@GuillaumeGomez
Copy link
Member Author

Renamed and fixed the typos/nits.

&& let Some(ty) = ty.as_type()
// We get its name in case it's a primitive with an associated MIN/MAX constant.
&& let Some(ty_name) = get_primitive_ty_name(ty)
&& match_def_path(cx, *def_id, &["core", "cmp", "Ord", method_name])
Copy link
Contributor

@samueltardieu samueltardieu Jan 10, 2025

Choose a reason for hiding this comment

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

I think you can use the diag item here.

Suggested change
&& match_def_path(cx, *def_id, &["core", "cmp", "Ord", method_name])
&& is_diag_trait_item(cx, *def_id, sym::Ord)

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact, you can even go simpler and test against the right method directly, both Ord::min and Ord::max are diagnostic items. This way you can use .item_name() directly as you know the method has a name.

    if let ty::FnDef(def_id, generics) = cast_from.kind()
        && cx.tcx.get_diagnostic_name(*def_id).is_some_and(|diag| diag == sym::cmp_ord_min || diag == sym::cmp_ord_max)
        // We get the type on which the `min`/`max` method of the `Ord` trait is implemented.
        && let [ty] = generics.as_slice()
        && let Some(ty) = ty.as_type()
        // We get its name in case it's a primitive with an associated MIN/MAX constant.
        && matches!(ty.kind(), ty::Char | ty::Int(_) | ty::Uint(_))
    {
        let mut applicability = Applicability::MaybeIncorrect;
        let from_snippet = snippet_with_applicability(cx, cast_expr.span, "..", &mut applicability);

        span_lint_and_then(
            cx,
            PRIMITIVE_METHOD_TO_NUMERIC_CAST,
            expr.span,
            format!("casting function pointer `{from_snippet}` to `{cast_to}`"),
            |diag| {
                diag.span_suggestion_verbose(
                    expr.span,
                    "did you mean to use the associated constant?",
                    format!(
                        "{ty}::{} as {cast_to}",
                        cx.tcx.item_name(*def_id).as_str().to_ascii_uppercase()
                    ),
                    applicability,
                );
            },
        );
    }

Comment on lines +34 to +36
&& let Some(ty) = ty.as_type()
// We get its name in case it's a primitive with an associated MIN/MAX constant.
&& let Some(ty_name) = get_primitive_ty_name(ty)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
&& let Some(ty) = ty.as_type()
// We get its name in case it's a primitive with an associated MIN/MAX constant.
&& let Some(ty_name) = get_primitive_ty_name(ty)
&& matches!(ty.kind(), ty::Char | ty::Int(_) | ty::Uint(_))

(floating point types don't implement ::MIN/::MAX)

and then you can use directly ty in your diagnostic, it will print as the type name, no need for the get_primitive_ty_name() function.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

@samueltardieu samueltardieu left a comment

Choose a reason for hiding this comment

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

I think this would deserve more tests, with the various types, as well as some other types implementing Ord not eligible to this lint.

&& let Some(ty) = ty.as_type()
// We get its name in case it's a primitive with an associated MIN/MAX constant.
&& let Some(ty_name) = get_primitive_ty_name(ty)
&& match_def_path(cx, *def_id, &["core", "cmp", "Ord", method_name])
Copy link
Contributor

Choose a reason for hiding this comment

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

In fact, you can even go simpler and test against the right method directly, both Ord::min and Ord::max are diagnostic items. This way you can use .item_name() directly as you know the method has a name.

    if let ty::FnDef(def_id, generics) = cast_from.kind()
        && cx.tcx.get_diagnostic_name(*def_id).is_some_and(|diag| diag == sym::cmp_ord_min || diag == sym::cmp_ord_max)
        // We get the type on which the `min`/`max` method of the `Ord` trait is implemented.
        && let [ty] = generics.as_slice()
        && let Some(ty) = ty.as_type()
        // We get its name in case it's a primitive with an associated MIN/MAX constant.
        && matches!(ty.kind(), ty::Char | ty::Int(_) | ty::Uint(_))
    {
        let mut applicability = Applicability::MaybeIncorrect;
        let from_snippet = snippet_with_applicability(cx, cast_expr.span, "..", &mut applicability);

        span_lint_and_then(
            cx,
            PRIMITIVE_METHOD_TO_NUMERIC_CAST,
            expr.span,
            format!("casting function pointer `{from_snippet}` to `{cast_to}`"),
            |diag| {
                diag.span_suggestion_verbose(
                    expr.span,
                    "did you mean to use the associated constant?",
                    format!(
                        "{ty}::{} as {cast_to}",
                        cx.tcx.item_name(*def_id).as_str().to_ascii_uppercase()
                    ),
                    applicability,
                );
            },
        );
    }

@@ -0,0 +1,24 @@
error: unknown lint: `clippy::primitive_method_to_numeric_cast`
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this supposed to happen?

@theemathas
Copy link
Contributor

Could this be extended to also cover conversions using from/into/try_from/try_into?

@theemathas
Copy link
Contributor

Floating point numbers also have inherent min/max methods even though they don't implement Ord https://doc.rust-lang.org/std/primitive.f32.html#method.max

@@ -754,6 +755,32 @@ declare_clippy_lint! {
"detects `as *mut _` and `as *const _` conversion"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for casts of a primitive method pointer to any integer type.
Copy link
Contributor

Choose a reason for hiding this comment

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

This communicates that casting any method on any integer type will cause this lint to fire, even though it checks for only min and max.

@samueltardieu
Copy link
Contributor

Floating point numbers also have inherent min/max methods even though they don't implement Ord https://doc.rust-lang.org/std/primitive.f32.html#method.max

Good point. I wonder whether:

  • the lint must cover them (probably)
  • the lint should cover any situation where we have a min/max method as well as MIN/MAX associated constants
  • the lint should cover any situation where we have a <x> method and an associated X constant (whatever x is) – I think this would be overkill because I couldn't find existing cases other than min and max on primitive types

@llogiq
Copy link
Contributor

llogiq commented Jan 26, 2025

the lint should cover any situation where we have a min/max method as well as MIN/MAX associated constants

I don't think we need this. People can name their functions and constants however they like, so we should not guess intent based on names here. If we cover the standard library, we'll be fine. If we really want, we can add a configuration entry for people to add their own items to the lint.

@GuillaumeGomez
Copy link
Member Author

Someone commented on the issue that min_value and max_value are also confusing and I agree so I plan to add them when I'll update this PR.

@meltyness
Copy link

Someone commented on the issue that min_value and max_value are also confusing and I agree so I plan to add them when I'll update this PR.

I took a whack at it, it seems like generics available to discern the base type available for the trait methods is not available for these deprecated methods, I made a workaround. I'm not sure it's the best way, but it seems to work with some cursory testing.

 else if let ty::FnDef(def_id, _generics) = cast_from.kind()
        && let Some(method_name) = cx.tcx.opt_item_name(*def_id)
        && let method_name = method_name.as_str()
        && (method_name == "min_value" || method_name == "max_value")
        && ["core", "num"].iter().map(|x| Symbol::intern(x)).zip(cx.get_def_path(*def_id).iter().copied()).all(|(a,b)| a == b)
    {
        let mut applicability = Applicability::MaybeIncorrect;
        let from_snippet = snippet_with_applicability(cx, cast_expr.span, "..", &mut applicability);

        let method_name = if method_name == "min_value" { "min" } else { "max" };
        let inferred_type = cx.get_def_path(*def_id).iter().copied().skip(2).next().unwrap();
        let inferred_type = inferred_type.as_str().split_whitespace().skip(1).next().unwrap();
        let inferred_type = inferred_type.chars().filter(|arg0: &char| char::is_alphanumeric(*arg0)).collect::<String>();
        span_lint_and_then(
            cx,
            CONFUSING_METHOD_TO_NUMERIC_CAST,
            expr.span,
            format!("casting function pointer `{from_snippet}` to `{cast_to}`"),
            |diag| {
                diag.span_suggestion_verbose(
                    expr.span,
                    "did you mean to use the associated constant?",
                    format!("{inferred_type:#?}::{} as {cast_to}", method_name.to_ascii_uppercase()),
                    applicability,
                );
            },
        );
    }

@meltyness
Copy link

There's also f32::minimum and f32::maximum i've discovered

@rustbot
Copy link
Collaborator

rustbot commented Mar 31, 2025

☔ The latest upstream changes (possibly d28d234) made this pull request unmergeable. Please resolve the merge conflicts.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Mar 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

u8::max as usize instead of u8::MAX as usize should warn
7 participants