-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Add new confusing_method_to_numeric_cast
lint
#13979
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
Conversation
359e815
to
042556c
Compare
primitive_method_to_numeric_cast
lintconfusing_method_to_numeric_cast
lint
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]) |
There was a problem hiding this comment.
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.
&& match_def_path(cx, *def_id, &["core", "cmp", "Ord", method_name]) | |
&& is_diag_trait_item(cx, *def_id, sym::Ord) |
There was a problem hiding this comment.
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,
);
},
);
}
&& 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
&& 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Floating point types do have MIN/MAX https://doc.rust-lang.org/std/primitive.f32.html#associatedconstant.MAX
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a test for floats then.
There was a problem hiding this 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]) |
There was a problem hiding this comment.
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,
);
},
);
}
Could this be extended to also cover conversions using from/into/try_from/try_into? |
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 |
clippy_lints/src/casts/mod.rs
Outdated
@@ -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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, clarified it.
Good point. I wonder whether:
|
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. |
Someone commented on the issue that |
I took a whack at it, it seems like 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,
);
},
);
} |
There's also |
This comment has been minimized.
This comment has been minimized.
042556c
to
e276af4
Compare
e276af4
to
06fa045
Compare
Finally took time to update this PR and to apply suggestions. I still don't think we should generalize this check between any methods and any existing associated constant so I didn't implement it but I suppose it can always be revisited at a later time. |
Thank you. Sorry this was waiting on me for so long. |
/// | ||
/// ### Why restrict this? | ||
/// Casting a function pointer to an integer can have surprising results and can occur | ||
/// accidentally if parentheses are omitted from a function call. If you aren't doing anything |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this comment incorrect? I don't think omitted parentheses are in the cases caught by this lint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's correct; the expression, i32::max
:
- omits parenthesis
- may be cast as an FP's (abstractly) location to, an
i64
, for example.
where the most likely intended effect would have been to obtain 2_147_483_647i32
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