diff options
| author | Philipp Krones <hello@philkrones.com> | 2023-04-11 15:31:08 +0200 |
|---|---|---|
| committer | Philipp Krones <hello@philkrones.com> | 2023-04-11 15:31:08 +0200 |
| commit | 0ca8ae5edd730d3c17b3f839dcff9aa906aa8c19 (patch) | |
| tree | d4de99e1486055040888a6096e95a87e9c44f6d0 /src/tools/clippy/clippy_lints | |
| parent | dfe024e1041d1cba1d3191024de3b7128c6734e6 (diff) | |
| parent | 83e42a2337dadac915c956d125f1d69132f36425 (diff) | |
| download | rust-0ca8ae5edd730d3c17b3f839dcff9aa906aa8c19.tar.gz rust-0ca8ae5edd730d3c17b3f839dcff9aa906aa8c19.zip | |
Merge commit '83e42a2337dadac915c956d125f1d69132f36425' into clippyup
Diffstat (limited to 'src/tools/clippy/clippy_lints')
37 files changed, 1468 insertions, 513 deletions
diff --git a/src/tools/clippy/clippy_lints/Cargo.toml b/src/tools/clippy/clippy_lints/Cargo.toml index 0b3846c1316..18e8bf77225 100644 --- a/src/tools/clippy/clippy_lints/Cargo.toml +++ b/src/tools/clippy/clippy_lints/Cargo.toml @@ -9,6 +9,7 @@ keywords = ["clippy", "lint", "plugin"] edition = "2021" [dependencies] +arrayvec = { version = "0.7", default-features = false } cargo_metadata = "0.15.3" clippy_utils = { path = "../clippy_utils" } declare_clippy_lint = { path = "../declare_clippy_lint" } diff --git a/src/tools/clippy/clippy_lints/src/booleans.rs b/src/tools/clippy/clippy_lints/src/booleans.rs index 29fde9336c0..455f0df7cd0 100644 --- a/src/tools/clippy/clippy_lints/src/booleans.rs +++ b/src/tools/clippy/clippy_lints/src/booleans.rs @@ -7,7 +7,7 @@ use rustc_ast::ast::LitKind; use rustc_errors::Applicability; use rustc_hir::intravisit::{walk_expr, FnKind, Visitor}; use rustc_hir::{BinOpKind, Body, Expr, ExprKind, FnDecl, UnOp}; -use rustc_lint::{LateContext, LateLintPass}; +use rustc_lint::{LateContext, LateLintPass, Level}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::def_id::LocalDefId; use rustc_span::source_map::Span; @@ -430,23 +430,25 @@ impl<'a, 'tcx> NonminimalBoolVisitor<'a, 'tcx> { } } let nonminimal_bool_lint = |suggestions: Vec<_>| { - span_lint_hir_and_then( - self.cx, - NONMINIMAL_BOOL, - e.hir_id, - e.span, - "this boolean expression can be simplified", - |diag| { - diag.span_suggestions( - e.span, - "try", - suggestions.into_iter(), - // nonminimal_bool can produce minimal but - // not human readable expressions (#3141) - Applicability::Unspecified, - ); - }, - ); + if self.cx.tcx.lint_level_at_node(NONMINIMAL_BOOL, e.hir_id).0 != Level::Allow { + span_lint_hir_and_then( + self.cx, + NONMINIMAL_BOOL, + e.hir_id, + e.span, + "this boolean expression can be simplified", + |diag| { + diag.span_suggestions( + e.span, + "try", + suggestions.into_iter(), + // nonminimal_bool can produce minimal but + // not human readable expressions (#3141) + Applicability::Unspecified, + ); + }, + ); + } }; if improvements.is_empty() { let mut visitor = NotSimplificationVisitor { cx: self.cx }; @@ -498,6 +500,7 @@ impl<'a, 'tcx> Visitor<'tcx> for NotSimplificationVisitor<'a, 'tcx> { if let ExprKind::Unary(UnOp::Not, inner) = &expr.kind && !inner.span.from_expansion() && let Some(suggestion) = simplify_not(self.cx, inner) + && self.cx.tcx.lint_level_at_node(NONMINIMAL_BOOL, expr.hir_id).0 != Level::Allow { span_lint_and_sugg( self.cx, diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_possible_truncation.rs b/src/tools/clippy/clippy_lints/src/casts/cast_possible_truncation.rs index 823970e35ab..95c2ecbf791 100644 --- a/src/tools/clippy/clippy_lints/src/casts/cast_possible_truncation.rs +++ b/src/tools/clippy/clippy_lints/src/casts/cast_possible_truncation.rs @@ -2,8 +2,9 @@ use clippy_utils::consts::{constant, Constant}; use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; use clippy_utils::expr_or_init; use clippy_utils::source::snippet; +use clippy_utils::sugg::Sugg; use clippy_utils::ty::{get_discriminant_value, is_isize_or_usize}; -use rustc_errors::{Applicability, SuggestionStyle}; +use rustc_errors::{Applicability, Diagnostic, SuggestionStyle}; use rustc_hir::def::{DefKind, Res}; use rustc_hir::{BinOpKind, Expr, ExprKind}; use rustc_lint::LateContext; @@ -163,19 +164,34 @@ pub(super) fn check( _ => return, }; - let name_of_cast_from = snippet(cx, cast_expr.span, ".."); - let cast_to_snip = snippet(cx, cast_to_span, ".."); - let suggestion = format!("{cast_to_snip}::try_from({name_of_cast_from})"); - span_lint_and_then(cx, CAST_POSSIBLE_TRUNCATION, expr.span, &msg, |diag| { diag.help("if this is intentional allow the lint with `#[allow(clippy::cast_possible_truncation)]` ..."); - diag.span_suggestion_with_style( - expr.span, - "... or use `try_from` and handle the error accordingly", - suggestion, - Applicability::Unspecified, - // always show the suggestion in a separate line - SuggestionStyle::ShowAlways, - ); + if !cast_from.is_floating_point() { + offer_suggestion(cx, expr, cast_expr, cast_to_span, diag); + } }); } + +fn offer_suggestion( + cx: &LateContext<'_>, + expr: &Expr<'_>, + cast_expr: &Expr<'_>, + cast_to_span: Span, + diag: &mut Diagnostic, +) { + let cast_to_snip = snippet(cx, cast_to_span, ".."); + let suggestion = if cast_to_snip == "_" { + format!("{}.try_into()", Sugg::hir(cx, cast_expr, "..").maybe_par()) + } else { + format!("{cast_to_snip}::try_from({})", Sugg::hir(cx, cast_expr, "..")) + }; + + diag.span_suggestion_with_style( + expr.span, + "... or use `try_from` and handle the error accordingly", + suggestion, + Applicability::Unspecified, + // always show the suggestion in a separate line + SuggestionStyle::ShowAlways, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/collection_is_never_read.rs b/src/tools/clippy/clippy_lints/src/collection_is_never_read.rs index 10f2bef268a..5e2eb5789f6 100644 --- a/src/tools/clippy/clippy_lints/src/collection_is_never_read.rs +++ b/src/tools/clippy/clippy_lints/src/collection_is_never_read.rs @@ -1,9 +1,9 @@ use clippy_utils::diagnostics::span_lint; -use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item}; use clippy_utils::visitors::for_each_expr_with_closures; use clippy_utils::{get_enclosing_block, get_parent_node, path_to_local_id}; use core::ops::ControlFlow; -use rustc_hir::{Block, ExprKind, HirId, Local, Node, PatKind}; +use rustc_hir::{Block, ExprKind, HirId, LangItem, Local, Node, PatKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::symbol::sym; @@ -44,7 +44,8 @@ declare_clippy_lint! { } declare_lint_pass!(CollectionIsNeverRead => [COLLECTION_IS_NEVER_READ]); -static COLLECTIONS: [Symbol; 10] = [ +// Add `String` here when it is added to diagnostic items +static COLLECTIONS: [Symbol; 9] = [ sym::BTreeMap, sym::BTreeSet, sym::BinaryHeap, @@ -52,7 +53,6 @@ static COLLECTIONS: [Symbol; 10] = [ sym::HashSet, sym::LinkedList, sym::Option, - sym::String, sym::Vec, sym::VecDeque, ]; @@ -60,8 +60,7 @@ static COLLECTIONS: [Symbol; 10] = [ impl<'tcx> LateLintPass<'tcx> for CollectionIsNeverRead { fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { // Look for local variables whose type is a container. Search surrounding bock for read access. - let ty = cx.typeck_results().pat_ty(local.pat); - if COLLECTIONS.iter().any(|&sym| is_type_diagnostic_item(cx, ty, sym)) + if match_acceptable_type(cx, local, &COLLECTIONS) && let PatKind::Binding(_, local_id, _, _) = local.pat.kind && let Some(enclosing_block) = get_enclosing_block(cx, local.hir_id) && has_no_read_access(cx, local_id, enclosing_block) @@ -71,6 +70,13 @@ impl<'tcx> LateLintPass<'tcx> for CollectionIsNeverRead { } } +fn match_acceptable_type(cx: &LateContext<'_>, local: &Local<'_>, collections: &[rustc_span::Symbol]) -> bool { + let ty = cx.typeck_results().pat_ty(local.pat); + collections.iter().any(|&sym| is_type_diagnostic_item(cx, ty, sym)) + // String type is a lang item but not a diagnostic item for now so we need a separate check + || is_type_lang_item(cx, ty, LangItem::String) +} + fn has_no_read_access<'tcx>(cx: &LateContext<'tcx>, id: HirId, block: &'tcx Block<'tcx>) -> bool { let mut has_access = false; let mut has_read_access = false; @@ -95,9 +101,9 @@ fn has_no_read_access<'tcx>(cx: &LateContext<'tcx>, id: HirId, block: &'tcx Bloc return ControlFlow::Continue(()); } - // Method call on `id` in a statement ignores any return value, so it's not a read access: + // Look for method call with receiver `id`. It might be a non-read access: // - // id.foo(...); // Not reading `id`. + // id.foo(args) // // Only assuming this for "official" methods defined on the type. For methods defined in extension // traits (identified as local, based on the orphan rule), pessimistically assume that they might @@ -105,11 +111,24 @@ fn has_no_read_access<'tcx>(cx: &LateContext<'tcx>, id: HirId, block: &'tcx Bloc if let Some(Node::Expr(parent)) = get_parent_node(cx.tcx, expr.hir_id) && let ExprKind::MethodCall(_, receiver, _, _) = parent.kind && path_to_local_id(receiver, id) - && let Some(Node::Stmt(..)) = get_parent_node(cx.tcx, parent.hir_id) && let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(parent.hir_id) && !method_def_id.is_local() { - return ControlFlow::Continue(()); + // The method call is a statement, so the return value is not used. That's not a read access: + // + // id.foo(args); + if let Some(Node::Stmt(..)) = get_parent_node(cx.tcx, parent.hir_id) { + return ControlFlow::Continue(()); + } + + // The method call is not a statement, so its return value is used somehow but its type is the + // unit type, so this is not a real read access. Examples: + // + // let y = x.clear(); + // println!("{:?}", x.clear()); + if cx.typeck_results().expr_ty(parent).is_unit() { + return ControlFlow::Continue(()); + } } // Any other access to `id` is a read access. Stop searching. diff --git a/src/tools/clippy/clippy_lints/src/declared_lints.rs b/src/tools/clippy/clippy_lints/src/declared_lints.rs index 8ca91301472..f24dab62780 100644 --- a/src/tools/clippy/clippy_lints/src/declared_lints.rs +++ b/src/tools/clippy/clippy_lints/src/declared_lints.rs @@ -218,6 +218,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::iter_not_returning_iterator::ITER_NOT_RETURNING_ITERATOR_INFO, crate::large_const_arrays::LARGE_CONST_ARRAYS_INFO, crate::large_enum_variant::LARGE_ENUM_VARIANT_INFO, + crate::large_futures::LARGE_FUTURES_INFO, crate::large_include_file::LARGE_INCLUDE_FILE_INFO, crate::large_stack_arrays::LARGE_STACK_ARRAYS_INFO, crate::len_zero::COMPARISON_TO_EMPTY_INFO, @@ -231,6 +232,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::let_with_type_underscore::LET_WITH_TYPE_UNDERSCORE_INFO, crate::lifetimes::EXTRA_UNUSED_LIFETIMES_INFO, crate::lifetimes::NEEDLESS_LIFETIMES_INFO, + crate::lines_filter_map_ok::LINES_FILTER_MAP_OK_INFO, crate::literal_representation::DECIMAL_LITERAL_REPRESENTATION_INFO, crate::literal_representation::INCONSISTENT_DIGIT_GROUPING_INFO, crate::literal_representation::LARGE_DIGIT_GROUPS_INFO, @@ -267,6 +269,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE_INFO, crate::manual_rem_euclid::MANUAL_REM_EUCLID_INFO, crate::manual_retain::MANUAL_RETAIN_INFO, + crate::manual_slice_size_calculation::MANUAL_SLICE_SIZE_CALCULATION_INFO, crate::manual_string_new::MANUAL_STRING_NEW_INFO, crate::manual_strip::MANUAL_STRIP_INFO, crate::map_unit_fn::OPTION_MAP_UNIT_FN_INFO, @@ -307,6 +310,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS_INFO, crate::methods::CHARS_LAST_CMP_INFO, crate::methods::CHARS_NEXT_CMP_INFO, + crate::methods::CLEAR_WITH_DRAIN_INFO, crate::methods::CLONED_INSTEAD_OF_COPIED_INFO, crate::methods::CLONE_DOUBLE_REF_INFO, crate::methods::CLONE_ON_COPY_INFO, @@ -565,6 +569,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::strings::STR_TO_STRING_INFO, crate::strings::TRIM_SPLIT_WHITESPACE_INFO, crate::strlen_on_c_strings::STRLEN_ON_C_STRINGS_INFO, + crate::suspicious_doc_comments::SUSPICIOUS_DOC_COMMENTS_INFO, crate::suspicious_operation_groupings::SUSPICIOUS_OPERATION_GROUPINGS_INFO, crate::suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL_INFO, crate::suspicious_trait_impl::SUSPICIOUS_OP_ASSIGN_IMPL_INFO, @@ -574,6 +579,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::swap_ptr_to_ref::SWAP_PTR_TO_REF_INFO, crate::tabs_in_doc_comments::TABS_IN_DOC_COMMENTS_INFO, crate::temporary_assignment::TEMPORARY_ASSIGNMENT_INFO, + crate::tests_outside_test_module::TESTS_OUTSIDE_TEST_MODULE_INFO, crate::to_digit_is_some::TO_DIGIT_IS_SOME_INFO, crate::trailing_empty_array::TRAILING_EMPTY_ARRAY_INFO, crate::trait_bounds::TRAIT_DUPLICATION_IN_BOUNDS_INFO, @@ -616,6 +622,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::unit_types::UNIT_CMP_INFO, crate::unnamed_address::FN_ADDRESS_COMPARISONS_INFO, crate::unnamed_address::VTABLE_ADDRESS_COMPARISONS_INFO, + crate::unnecessary_box_returns::UNNECESSARY_BOX_RETURNS_INFO, crate::unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS_INFO, crate::unnecessary_self_imports::UNNECESSARY_SELF_IMPORTS_INFO, crate::unnecessary_struct_initialization::UNNECESSARY_STRUCT_INITIALIZATION_INFO, diff --git a/src/tools/clippy/clippy_lints/src/disallowed_script_idents.rs b/src/tools/clippy/clippy_lints/src/disallowed_script_idents.rs index 084190f0013..c9fad98e437 100644 --- a/src/tools/clippy/clippy_lints/src/disallowed_script_idents.rs +++ b/src/tools/clippy/clippy_lints/src/disallowed_script_idents.rs @@ -32,7 +32,7 @@ declare_clippy_lint! { /// ### Example /// ```rust /// // Assuming that `clippy.toml` contains the following line: - /// // allowed-locales = ["Latin", "Cyrillic"] + /// // allowed-scripts = ["Latin", "Cyrillic"] /// let counter = 10; // OK, latin is allowed. /// let счётчик = 10; // OK, cyrillic is allowed. /// let zähler = 10; // OK, it's still latin. diff --git a/src/tools/clippy/clippy_lints/src/explicit_write.rs b/src/tools/clippy/clippy_lints/src/explicit_write.rs index c0ea6f338a2..315df6c714f 100644 --- a/src/tools/clippy/clippy_lints/src/explicit_write.rs +++ b/src/tools/clippy/clippy_lints/src/explicit_write.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::macros::FormatArgsExpn; +use clippy_utils::macros::{find_format_args, format_args_inputs_span}; use clippy_utils::source::snippet_with_applicability; use clippy_utils::{is_expn_of, match_function_call, paths}; use if_chain::if_chain; @@ -8,7 +8,7 @@ use rustc_hir::def::Res; use rustc_hir::{BindingAnnotation, Block, BlockCheckMode, Expr, ExprKind, Node, PatKind, QPath, Stmt, StmtKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; -use rustc_span::sym; +use rustc_span::{sym, ExpnId}; declare_clippy_lint! { /// ### What it does @@ -43,23 +43,22 @@ declare_lint_pass!(ExplicitWrite => [EXPLICIT_WRITE]); impl<'tcx> LateLintPass<'tcx> for ExplicitWrite { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - if_chain! { - // match call to unwrap - if let ExprKind::MethodCall(unwrap_fun, write_call, [], _) = expr.kind; - if unwrap_fun.ident.name == sym::unwrap; + // match call to unwrap + if let ExprKind::MethodCall(unwrap_fun, write_call, [], _) = expr.kind + && unwrap_fun.ident.name == sym::unwrap // match call to write_fmt - if let ExprKind::MethodCall(write_fun, write_recv, [write_arg], _) = look_in_block(cx, &write_call.kind); - if write_fun.ident.name == sym!(write_fmt); + && let ExprKind::MethodCall(write_fun, write_recv, [write_arg], _) = look_in_block(cx, &write_call.kind) + && write_fun.ident.name == sym!(write_fmt) // match calls to std::io::stdout() / std::io::stderr () - if let Some(dest_name) = if match_function_call(cx, write_recv, &paths::STDOUT).is_some() { + && let Some(dest_name) = if match_function_call(cx, write_recv, &paths::STDOUT).is_some() { Some("stdout") } else if match_function_call(cx, write_recv, &paths::STDERR).is_some() { Some("stderr") } else { None - }; - if let Some(format_args) = FormatArgsExpn::parse(cx, write_arg); - then { + } + { + find_format_args(cx, write_arg, ExpnId::root(), |format_args| { let calling_macro = // ordering is important here, since `writeln!` uses `write!` internally if is_expn_of(write_call.span, "writeln").is_some() { @@ -92,7 +91,7 @@ impl<'tcx> LateLintPass<'tcx> for ExplicitWrite { let mut applicability = Applicability::MachineApplicable; let inputs_snippet = snippet_with_applicability( cx, - format_args.inputs_span(), + format_args_inputs_span(format_args), "..", &mut applicability, ); @@ -104,8 +103,8 @@ impl<'tcx> LateLintPass<'tcx> for ExplicitWrite { "try this", format!("{prefix}{sugg_mac}!({inputs_snippet})"), applicability, - ) - } + ); + }); } } } diff --git a/src/tools/clippy/clippy_lints/src/extra_unused_type_parameters.rs b/src/tools/clippy/clippy_lints/src/extra_unused_type_parameters.rs index 20565e1d232..eeb4de8b58f 100644 --- a/src/tools/clippy/clippy_lints/src/extra_unused_type_parameters.rs +++ b/src/tools/clippy/clippy_lints/src/extra_unused_type_parameters.rs @@ -1,10 +1,10 @@ -use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then}; use clippy_utils::trait_ref_of_method; -use rustc_data_structures::fx::FxHashMap; -use rustc_errors::MultiSpan; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_errors::Applicability; use rustc_hir::intravisit::{walk_impl_item, walk_item, walk_param_bound, walk_ty, Visitor}; use rustc_hir::{ - BodyId, ExprKind, GenericBound, GenericParamKind, Generics, ImplItem, ImplItemKind, Item, ItemKind, + BodyId, ExprKind, GenericBound, GenericParam, GenericParamKind, Generics, ImplItem, ImplItemKind, Item, ItemKind, PredicateOrigin, Ty, TyKind, WherePredicate, }; use rustc_lint::{LateContext, LateLintPass, LintContext}; @@ -53,13 +53,19 @@ impl ExtraUnusedTypeParameters { } } - /// Don't lint external macros or functions with empty bodies. Also, don't lint public items if - /// the `avoid_breaking_exported_api` config option is set. - fn check_false_positive(&self, cx: &LateContext<'_>, span: Span, def_id: LocalDefId, body_id: BodyId) -> bool { + /// Don't lint external macros or functions with empty bodies. Also, don't lint exported items + /// if the `avoid_breaking_exported_api` config option is set. + fn is_empty_exported_or_macro( + &self, + cx: &LateContext<'_>, + span: Span, + def_id: LocalDefId, + body_id: BodyId, + ) -> bool { let body = cx.tcx.hir().body(body_id).value; let fn_empty = matches!(&body.kind, ExprKind::Block(blk, None) if blk.stmts.is_empty() && blk.expr.is_none()); let is_exported = cx.effective_visibilities.is_exported(def_id); - in_external_macro(cx.sess(), span) || (self.avoid_breaking_exported_api && is_exported) || fn_empty + in_external_macro(cx.sess(), span) || fn_empty || (is_exported && self.avoid_breaking_exported_api) } } @@ -69,85 +75,129 @@ impl_lint_pass!(ExtraUnusedTypeParameters => [EXTRA_UNUSED_TYPE_PARAMETERS]); /// trait bounds those parameters have. struct TypeWalker<'cx, 'tcx> { cx: &'cx LateContext<'tcx>, - /// Collection of all the function's type parameters. + /// Collection of the function's type parameters. Once the function has been walked, this will + /// contain only unused type parameters. ty_params: FxHashMap<DefId, Span>, - /// Collection of any (inline) trait bounds corresponding to each type parameter. - bounds: FxHashMap<DefId, Span>, + /// Collection of any inline trait bounds corresponding to each type parameter. + inline_bounds: FxHashMap<DefId, Span>, + /// Collection of any type parameters with trait bounds that appear in a where clause. + where_bounds: FxHashSet<DefId>, /// The entire `Generics` object of the function, useful for querying purposes. generics: &'tcx Generics<'tcx>, - /// The value of this will remain `true` if *every* parameter: - /// 1. Is a type parameter, and - /// 2. Goes unused in the function. - /// Otherwise, if any type parameters end up being used, or if any lifetime or const-generic - /// parameters are present, this will be set to `false`. - all_params_unused: bool, } impl<'cx, 'tcx> TypeWalker<'cx, 'tcx> { fn new(cx: &'cx LateContext<'tcx>, generics: &'tcx Generics<'tcx>) -> Self { - let mut all_params_unused = true; let ty_params = generics .params .iter() - .filter_map(|param| { - if let GenericParamKind::Type { synthetic, .. } = param.kind { - (!synthetic).then_some((param.def_id.into(), param.span)) - } else { - if !param.is_elided_lifetime() { - all_params_unused = false; - } - None - } + .filter_map(|param| match param.kind { + GenericParamKind::Type { synthetic, .. } if !synthetic => Some((param.def_id.into(), param.span)), + _ => None, }) .collect(); Self { cx, ty_params, - bounds: FxHashMap::default(), + inline_bounds: FxHashMap::default(), + where_bounds: FxHashSet::default(), generics, - all_params_unused, } } - fn mark_param_used(&mut self, def_id: DefId) { - if self.ty_params.remove(&def_id).is_some() { - self.all_params_unused = false; - } + fn get_bound_span(&self, param: &'tcx GenericParam<'tcx>) -> Span { + self.inline_bounds + .get(¶m.def_id.to_def_id()) + .map_or(param.span, |bound_span| param.span.with_hi(bound_span.hi())) + } + + fn emit_help(&self, spans: Vec<Span>, msg: &str, help: &'static str) { + span_lint_and_help(self.cx, EXTRA_UNUSED_TYPE_PARAMETERS, spans, msg, None, help); + } + + fn emit_sugg(&self, spans: Vec<Span>, msg: &str, help: &'static str) { + let suggestions: Vec<(Span, String)> = spans.iter().copied().zip(std::iter::repeat(String::new())).collect(); + span_lint_and_then(self.cx, EXTRA_UNUSED_TYPE_PARAMETERS, spans, msg, |diag| { + diag.multipart_suggestion(help, suggestions, Applicability::MachineApplicable); + }); } fn emit_lint(&self) { - let (msg, help) = match self.ty_params.len() { + let explicit_params = self + .generics + .params + .iter() + .filter(|param| !param.is_elided_lifetime() && !param.is_impl_trait()) + .collect::<Vec<_>>(); + + let extra_params = explicit_params + .iter() + .enumerate() + .filter(|(_, param)| self.ty_params.contains_key(¶m.def_id.to_def_id())) + .collect::<Vec<_>>(); + + let (msg, help) = match extra_params.len() { 0 => return, 1 => ( - "type parameter goes unused in function definition", + format!( + "type parameter `{}` goes unused in function definition", + extra_params[0].1.name.ident() + ), "consider removing the parameter", ), _ => ( - "type parameters go unused in function definition", + format!( + "type parameters go unused in function definition: {}", + extra_params + .iter() + .map(|(_, param)| param.name.ident().to_string()) + .collect::<Vec<_>>() + .join(", ") + ), "consider removing the parameters", ), }; - let source_map = self.cx.sess().source_map(); - let span = if self.all_params_unused { - self.generics.span.into() // Remove the entire list of generics + // If any parameters are bounded in where clauses, don't try to form a suggestion. + // Otherwise, the leftover where bound would produce code that wouldn't compile. + if extra_params + .iter() + .any(|(_, param)| self.where_bounds.contains(¶m.def_id.to_def_id())) + { + let spans = extra_params + .iter() + .map(|(_, param)| self.get_bound_span(param)) + .collect::<Vec<_>>(); + self.emit_help(spans, &msg, help); } else { - MultiSpan::from_spans( - self.ty_params + let spans = if explicit_params.len() == extra_params.len() { + vec![self.generics.span] // Remove the entire list of generics + } else { + let mut end: Option<LocalDefId> = None; + extra_params .iter() - .map(|(def_id, &span)| { - // Extend the span past any trait bounds, and include the comma at the end. - let span_to_extend = self.bounds.get(def_id).copied().map_or(span, Span::shrink_to_hi); - let comma_range = source_map.span_extend_to_next_char(span_to_extend, '>', false); - let comma_span = source_map.span_through_char(comma_range, ','); - span.with_hi(comma_span.hi()) + .rev() + .map(|(idx, param)| { + if let Some(next) = explicit_params.get(idx + 1) && end != Some(next.def_id) { + // Extend the current span forward, up until the next param in the list. + param.span.until(next.span) + } else { + // Extend the current span back to include the comma following the previous + // param. If the span of the next param in the list has already been + // extended, we continue the chain. This is why we're iterating in reverse. + end = Some(param.def_id); + + // idx will never be 0, else we'd be removing the entire list of generics + let prev = explicit_params[idx - 1]; + let prev_span = self.get_bound_span(prev); + self.get_bound_span(param).with_lo(prev_span.hi()) + } }) - .collect(), - ) + .collect() + }; + self.emit_sugg(spans, &msg, help); }; - - span_lint_and_help(self.cx, EXTRA_UNUSED_TYPE_PARAMETERS, span, msg, None, help); } } @@ -162,7 +212,7 @@ impl<'cx, 'tcx> Visitor<'tcx> for TypeWalker<'cx, 'tcx> { fn visit_ty(&mut self, t: &'tcx Ty<'tcx>) { if let Some((def_id, _)) = t.peel_refs().as_generic_param() { - self.mark_param_used(def_id); + self.ty_params.remove(&def_id); } else if let TyKind::OpaqueDef(id, _, _) = t.kind { // Explicitly walk OpaqueDef. Normally `walk_ty` would do the job, but it calls // `visit_nested_item`, which checks that `Self::NestedFilter::INTER` is set. We're @@ -176,9 +226,18 @@ impl<'cx, 'tcx> Visitor<'tcx> for TypeWalker<'cx, 'tcx> { fn visit_where_predicate(&mut self, predicate: &'tcx WherePredicate<'tcx>) { if let WherePredicate::BoundPredicate(predicate) = predicate { - // Collect spans for any bounds on type parameters. We only keep bounds that appear in - // the list of generics (not in a where-clause). + // Collect spans for any bounds on type parameters. if let Some((def_id, _)) = predicate.bounded_ty.peel_refs().as_generic_param() { + match predicate.origin { + PredicateOrigin::GenericParam => { + self.inline_bounds.insert(def_id, predicate.span); + }, + PredicateOrigin::WhereClause => { + self.where_bounds.insert(def_id); + }, + PredicateOrigin::ImplTrait => (), + } + // If the bound contains non-public traits, err on the safe side and don't lint the // corresponding parameter. if !predicate @@ -187,12 +246,10 @@ impl<'cx, 'tcx> Visitor<'tcx> for TypeWalker<'cx, 'tcx> { .filter_map(bound_to_trait_def_id) .all(|id| self.cx.effective_visibilities.is_exported(id)) { - self.mark_param_used(def_id); - } else if let PredicateOrigin::GenericParam = predicate.origin { - self.bounds.insert(def_id, predicate.span); + self.ty_params.remove(&def_id); } } - // Only walk the right-hand side of where-bounds + // Only walk the right-hand side of where bounds for bound in predicate.bounds { walk_param_bound(self, bound); } @@ -207,7 +264,7 @@ impl<'cx, 'tcx> Visitor<'tcx> for TypeWalker<'cx, 'tcx> { impl<'tcx> LateLintPass<'tcx> for ExtraUnusedTypeParameters { fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { if let ItemKind::Fn(_, generics, body_id) = item.kind - && !self.check_false_positive(cx, item.span, item.owner_id.def_id, body_id) + && !self.is_empty_exported_or_macro(cx, item.span, item.owner_id.def_id, body_id) { let mut walker = TypeWalker::new(cx, generics); walk_item(&mut walker, item); @@ -219,7 +276,7 @@ impl<'tcx> LateLintPass<'tcx> for ExtraUnusedTypeParameters { // Only lint on inherent methods, not trait methods. if let ImplItemKind::Fn(.., body_id) = item.kind && trait_ref_of_method(cx, item.owner_id.def_id).is_none() - && !self.check_false_positive(cx, item.span, item.owner_id.def_id, body_id) + && !self.is_empty_exported_or_macro(cx, item.span, item.owner_id.def_id, body_id) { let mut walker = TypeWalker::new(cx, item.generics); walk_impl_item(&mut walker, item); diff --git a/src/tools/clippy/clippy_lints/src/format.rs b/src/tools/clippy/clippy_lints/src/format.rs index 8040938c626..d34d6e9279e 100644 --- a/src/tools/clippy/clippy_lints/src/format.rs +++ b/src/tools/clippy/clippy_lints/src/format.rs @@ -1,14 +1,13 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn}; -use clippy_utils::source::snippet_with_context; +use clippy_utils::macros::{find_format_arg_expr, find_format_args, root_macro_call_first_node}; +use clippy_utils::source::{snippet_opt, snippet_with_context}; use clippy_utils::sugg::Sugg; -use if_chain::if_chain; +use rustc_ast::{FormatArgsPiece, FormatOptions, FormatTrait}; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty; use rustc_session::{declare_lint_pass, declare_tool_lint}; -use rustc_span::symbol::kw; use rustc_span::{sym, Span}; declare_clippy_lint! { @@ -44,55 +43,53 @@ declare_lint_pass!(UselessFormat => [USELESS_FORMAT]); impl<'tcx> LateLintPass<'tcx> for UselessFormat { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - let (format_args, call_site) = if_chain! { - if let Some(macro_call) = root_macro_call_first_node(cx, expr); - if cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id); - if let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, macro_call.expn); - then { - (format_args, macro_call.span) - } else { - return - } - }; + let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return }; + if !cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id) { + return; + } + + find_format_args(cx, expr, macro_call.expn, |format_args| { + let mut applicability = Applicability::MachineApplicable; + let call_site = macro_call.span; - let mut applicability = Applicability::MachineApplicable; - if format_args.args.is_empty() { - match *format_args.format_string.parts { - [] => span_useless_format_empty(cx, call_site, "String::new()".to_owned(), applicability), - [_] => { + match (format_args.arguments.all_args(), &format_args.template[..]) { + ([], []) => span_useless_format_empty(cx, call_site, "String::new()".to_owned(), applicability), + ([], [_]) => { // Simulate macro expansion, converting {{ and }} to { and }. - let s_expand = format_args.format_string.snippet.replace("{{", "{").replace("}}", "}"); + let Some(snippet) = snippet_opt(cx, format_args.span) else { return }; + let s_expand = snippet.replace("{{", "{").replace("}}", "}"); let sugg = format!("{s_expand}.to_string()"); span_useless_format(cx, call_site, sugg, applicability); }, - [..] => {}, - } - } else if let [arg] = &*format_args.args { - let value = arg.param.value; - if_chain! { - if format_args.format_string.parts == [kw::Empty]; - if arg.format.is_default(); - if match cx.typeck_results().expr_ty(value).peel_refs().kind() { - ty::Adt(adt, _) => Some(adt.did()) == cx.tcx.lang_items().string(), - ty::Str => true, - _ => false, - }; - then { - let is_new_string = match value.kind { - ExprKind::Binary(..) => true, - ExprKind::MethodCall(path, ..) => path.ident.name == sym::to_string, - _ => false, - }; - let sugg = if is_new_string { - snippet_with_context(cx, value.span, call_site.ctxt(), "..", &mut applicability).0.into_owned() - } else { - let sugg = Sugg::hir_with_context(cx, value, call_site.ctxt(), "<arg>", &mut applicability); - format!("{}.to_string()", sugg.maybe_par()) - }; - span_useless_format(cx, call_site, sugg, applicability); - } + ([arg], [piece]) => { + if let Ok(value) = find_format_arg_expr(expr, arg) + && let FormatArgsPiece::Placeholder(placeholder) = piece + && placeholder.format_trait == FormatTrait::Display + && placeholder.format_options == FormatOptions::default() + && match cx.typeck_results().expr_ty(value).peel_refs().kind() { + ty::Adt(adt, _) => Some(adt.did()) == cx.tcx.lang_items().string(), + ty::Str => true, + _ => false, + } + { + let is_new_string = match value.kind { + ExprKind::Binary(..) => true, + ExprKind::MethodCall(path, ..) => path.ident.name == sym::to_string, + _ => false, + }; + let sugg = if is_new_string { + snippet_with_context(cx, value.span, call_site.ctxt(), "..", &mut applicability).0.into_owned() + } else { + let sugg = Sugg::hir_with_context(cx, value, call_site.ctxt(), "<arg>", &mut applicability); + format!("{}.to_string()", sugg.maybe_par()) + }; + span_useless_format(cx, call_site, sugg, applicability); + + } + }, + _ => {}, } - }; + }); } } diff --git a/src/tools/clippy/clippy_lints/src/format_args.rs b/src/tools/clippy/clippy_lints/src/format_args.rs index c511d85e9cf..08e45ed7d0e 100644 --- a/src/tools/clippy/clippy_lints/src/format_args.rs +++ b/src/tools/clippy/clippy_lints/src/format_args.rs @@ -1,27 +1,31 @@ +use arrayvec::ArrayVec; use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::is_diag_trait_item; -use clippy_utils::macros::FormatParamKind::{Implicit, Named, NamedInline, Numbered, Starred}; use clippy_utils::macros::{ - is_assert_macro, is_format_macro, is_panic, root_macro_call, Count, FormatArg, FormatArgsExpn, FormatParam, - FormatParamUsage, + find_format_arg_expr, find_format_args, format_arg_removal_span, format_placeholder_format_span, is_assert_macro, + is_format_macro, is_panic, root_macro_call, root_macro_call_first_node, FormatParamUsage, }; use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::snippet_opt; use clippy_utils::ty::{implements_trait, is_type_lang_item}; use if_chain::if_chain; use itertools::Itertools; +use rustc_ast::{ + FormatArgPosition, FormatArgPositionKind, FormatArgsPiece, FormatArgumentKind, FormatCount, FormatOptions, + FormatPlaceholder, FormatTrait, +}; use rustc_errors::{ Applicability, SuggestionStyle::{CompletelyHidden, ShowCode}, }; -use rustc_hir::{Expr, ExprKind, HirId, LangItem, QPath}; +use rustc_hir::{Expr, ExprKind, LangItem}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::ty::adjustment::{Adjust, Adjustment}; use rustc_middle::ty::Ty; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::def_id::DefId; use rustc_span::edition::Edition::Edition2021; -use rustc_span::{sym, ExpnData, ExpnKind, Span, Symbol}; +use rustc_span::{sym, Span, Symbol}; declare_clippy_lint! { /// ### What it does @@ -184,72 +188,79 @@ impl FormatArgs { impl<'tcx> LateLintPass<'tcx> for FormatArgs { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { - if let Some(format_args) = FormatArgsExpn::parse(cx, expr) - && let expr_expn_data = expr.span.ctxt().outer_expn_data() - && let outermost_expn_data = outermost_expn_data(expr_expn_data) - && let Some(macro_def_id) = outermost_expn_data.macro_def_id - && is_format_macro(cx, macro_def_id) - && let ExpnKind::Macro(_, name) = outermost_expn_data.kind - { - for arg in &format_args.args { - check_unused_format_specifier(cx, arg); - if !arg.format.is_default() { - continue; - } - if is_aliased(&format_args, arg.param.value.hir_id) { - continue; + let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return }; + if !is_format_macro(cx, macro_call.def_id) { + return; + } + let name = cx.tcx.item_name(macro_call.def_id); + + find_format_args(cx, expr, macro_call.expn, |format_args| { + for piece in &format_args.template { + if let FormatArgsPiece::Placeholder(placeholder) = piece + && let Ok(index) = placeholder.argument.index + && let Some(arg) = format_args.arguments.all_args().get(index) + { + let arg_expr = find_format_arg_expr(expr, arg); + + check_unused_format_specifier(cx, placeholder, arg_expr); + + if placeholder.format_trait != FormatTrait::Display + || placeholder.format_options != FormatOptions::default() + || is_aliased(format_args, index) + { + continue; + } + + if let Ok(arg_hir_expr) = arg_expr { + check_format_in_format_args(cx, macro_call.span, name, arg_hir_expr); + check_to_string_in_format_args(cx, name, arg_hir_expr); + } } - check_format_in_format_args(cx, outermost_expn_data.call_site, name, arg.param.value); - check_to_string_in_format_args(cx, name, arg.param.value); } + if self.msrv.meets(msrvs::FORMAT_ARGS_CAPTURE) { - check_uninlined_args(cx, &format_args, outermost_expn_data.call_site, macro_def_id, self.ignore_mixed); + check_uninlined_args(cx, format_args, macro_call.span, macro_call.def_id, self.ignore_mixed); } - } + }); } extract_msrv_attr!(LateContext); } -fn check_unused_format_specifier(cx: &LateContext<'_>, arg: &FormatArg<'_>) { - let param_ty = cx.typeck_results().expr_ty(arg.param.value).peel_refs(); +fn check_unused_format_specifier( + cx: &LateContext<'_>, + placeholder: &FormatPlaceholder, + arg_expr: Result<&Expr<'_>, &rustc_ast::Expr>, +) { + let ty_or_ast_expr = arg_expr.map(|expr| cx.typeck_results().expr_ty(expr).peel_refs()); - if let Count::Implied(Some(mut span)) = arg.format.precision - && !span.is_empty() - { - span_lint_and_then( - cx, - UNUSED_FORMAT_SPECS, - span, - "empty precision specifier has no effect", - |diag| { - if param_ty.is_floating_point() { - diag.note("a precision specifier is not required to format floats"); - } + let is_format_args = match ty_or_ast_expr { + Ok(ty) => is_type_lang_item(cx, ty, LangItem::FormatArguments), + Err(expr) => matches!(expr.peel_parens_and_refs().kind, rustc_ast::ExprKind::FormatArgs(_)), + }; - if arg.format.is_default() { - // If there's no other specifiers remove the `:` too - span = arg.format_span(); - } + let options = &placeholder.format_options; - diag.span_suggestion_verbose(span, "remove the `.`", "", Applicability::MachineApplicable); - }, - ); - } + let arg_span = match arg_expr { + Ok(expr) => expr.span, + Err(expr) => expr.span, + }; - if is_type_lang_item(cx, param_ty, LangItem::FormatArguments) && !arg.format.is_default_for_trait() { + if let Some(placeholder_span) = placeholder.span + && is_format_args + && *options != FormatOptions::default() + { span_lint_and_then( cx, UNUSED_FORMAT_SPECS, - arg.span, + placeholder_span, "format specifiers have no effect on `format_args!()`", |diag| { - let mut suggest_format = |spec, span| { + let mut suggest_format = |spec| { let message = format!("for the {spec} to apply consider using `format!()`"); - if let Some(mac_call) = root_macro_call(arg.param.value.span) + if let Some(mac_call) = root_macro_call(arg_span) && cx.tcx.is_diagnostic_item(sym::format_args_macro, mac_call.def_id) - && arg.span.eq_ctxt(mac_call.span) { diag.span_suggestion( cx.sess().source_map().span_until_char(mac_call.span, '!'), @@ -257,25 +268,27 @@ fn check_unused_format_specifier(cx: &LateContext<'_>, arg: &FormatArg<'_>) { "format", Applicability::MaybeIncorrect, ); - } else if let Some(span) = span { - diag.span_help(span, message); + } else { + diag.help(message); } }; - if !arg.format.width.is_implied() { - suggest_format("width", arg.format.width.span()); + if options.width.is_some() { + suggest_format("width"); } - if !arg.format.precision.is_implied() { - suggest_format("precision", arg.format.precision.span()); + if options.precision.is_some() { + suggest_format("precision"); } - diag.span_suggestion_verbose( - arg.format_span(), - "if the current behavior is intentional, remove the format specifiers", - "", - Applicability::MaybeIncorrect, - ); + if let Some(format_span) = format_placeholder_format_span(placeholder) { + diag.span_suggestion_verbose( + format_span, + "if the current behavior is intentional, remove the format specifiers", + "", + Applicability::MaybeIncorrect, + ); + } }, ); } @@ -283,12 +296,12 @@ fn check_unused_format_specifier(cx: &LateContext<'_>, arg: &FormatArg<'_>) { fn check_uninlined_args( cx: &LateContext<'_>, - args: &FormatArgsExpn<'_>, + args: &rustc_ast::FormatArgs, call_site: Span, def_id: DefId, ignore_mixed: bool, ) { - if args.format_string.span.from_expansion() { + if args.span.from_expansion() { return; } if call_site.edition() < Edition2021 && (is_panic(cx, def_id) || is_assert_macro(cx, def_id)) { @@ -303,7 +316,13 @@ fn check_uninlined_args( // we cannot remove any other arguments in the format string, // because the index numbers might be wrong after inlining. // Example of an un-inlinable format: print!("{}{1}", foo, 2) - if !args.params().all(|p| check_one_arg(args, &p, &mut fixes, ignore_mixed)) || fixes.is_empty() { + for (pos, usage) in format_arg_positions(args) { + if !check_one_arg(args, pos, usage, &mut fixes, ignore_mixed) { + return; + } + } + + if fixes.is_empty() { return; } @@ -332,47 +351,40 @@ fn check_uninlined_args( } fn check_one_arg( - args: &FormatArgsExpn<'_>, - param: &FormatParam<'_>, + args: &rustc_ast::FormatArgs, + pos: &FormatArgPosition, + usage: FormatParamUsage, fixes: &mut Vec<(Span, String)>, ignore_mixed: bool, ) -> bool { - if matches!(param.kind, Implicit | Starred | Named(_) | Numbered) - && let ExprKind::Path(QPath::Resolved(None, path)) = param.value.kind - && let [segment] = path.segments + let index = pos.index.unwrap(); + let arg = &args.arguments.all_args()[index]; + + if !matches!(arg.kind, FormatArgumentKind::Captured(_)) + && let rustc_ast::ExprKind::Path(None, path) = &arg.expr.kind + && let [segment] = path.segments.as_slice() && segment.args.is_none() - && let Some(arg_span) = args.value_with_prev_comma_span(param.value.hir_id) + && let Some(arg_span) = format_arg_removal_span(args, index) + && let Some(pos_span) = pos.span { - let replacement = match param.usage { + let replacement = match usage { FormatParamUsage::Argument => segment.ident.name.to_string(), FormatParamUsage::Width => format!("{}$", segment.ident.name), FormatParamUsage::Precision => format!(".{}$", segment.ident.name), }; - fixes.push((param.span, replacement)); + fixes.push((pos_span, replacement)); fixes.push((arg_span, String::new())); true // successful inlining, continue checking } else { // Do not continue inlining (return false) in case // * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)` // * if allow_mixed_uninlined_format_args is false and this arg hasn't been inlined already - param.kind != Numbered && (!ignore_mixed || matches!(param.kind, NamedInline(_))) - } -} - -fn outermost_expn_data(expn_data: ExpnData) -> ExpnData { - if expn_data.call_site.from_expansion() { - outermost_expn_data(expn_data.call_site.ctxt().outer_expn_data()) - } else { - expn_data + pos.kind != FormatArgPositionKind::Number + && (!ignore_mixed || matches!(arg.kind, FormatArgumentKind::Captured(_))) } } -fn check_format_in_format_args( - cx: &LateContext<'_>, - call_site: Span, - name: Symbol, - arg: &Expr<'_>, -) { +fn check_format_in_format_args(cx: &LateContext<'_>, call_site: Span, name: Symbol, arg: &Expr<'_>) { let expn_data = arg.span.ctxt().outer_expn_data(); if expn_data.call_site.from_expansion() { return; @@ -443,9 +455,33 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex } } -/// Returns true if `hir_id` is referred to by multiple format params -fn is_aliased(args: &FormatArgsExpn<'_>, hir_id: HirId) -> bool { - args.params().filter(|param| param.value.hir_id == hir_id).at_most_one().is_err() +fn format_arg_positions( + format_args: &rustc_ast::FormatArgs, +) -> impl Iterator<Item = (&FormatArgPosition, FormatParamUsage)> { + format_args.template.iter().flat_map(|piece| match piece { + FormatArgsPiece::Placeholder(placeholder) => { + let mut positions = ArrayVec::<_, 3>::new(); + + positions.push((&placeholder.argument, FormatParamUsage::Argument)); + if let Some(FormatCount::Argument(position)) = &placeholder.format_options.width { + positions.push((position, FormatParamUsage::Width)); + } + if let Some(FormatCount::Argument(position)) = &placeholder.format_options.precision { + positions.push((position, FormatParamUsage::Precision)); + } + + positions + }, + FormatArgsPiece::Literal(_) => ArrayVec::new(), + }) +} + +/// Returns true if the format argument at `index` is referred to by multiple format params +fn is_aliased(format_args: &rustc_ast::FormatArgs, index: usize) -> bool { + format_arg_positions(format_args) + .filter(|(position, _)| position.index == Ok(index)) + .at_most_one() + .is_err() } fn count_needed_derefs<'tcx, I>(mut ty: Ty<'tcx>, mut iter: I) -> (usize, Ty<'tcx>) @@ -455,7 +491,11 @@ where let mut n_total = 0; let mut n_needed = 0; loop { - if let Some(Adjustment { kind: Adjust::Deref(overloaded_deref), target }) = iter.next() { + if let Some(Adjustment { + kind: Adjust::Deref(overloaded_deref), + target, + }) = iter.next() + { n_total += 1; if overloaded_deref.is_some() { n_needed = n_total; diff --git a/src/tools/clippy/clippy_lints/src/format_impl.rs b/src/tools/clippy/clippy_lints/src/format_impl.rs index ed1342a5465..e3ddbfb5981 100644 --- a/src/tools/clippy/clippy_lints/src/format_impl.rs +++ b/src/tools/clippy/clippy_lints/src/format_impl.rs @@ -1,11 +1,13 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; -use clippy_utils::macros::{is_format_macro, root_macro_call_first_node, FormatArg, FormatArgsExpn}; +use clippy_utils::macros::{find_format_arg_expr, find_format_args, is_format_macro, root_macro_call_first_node}; use clippy_utils::{get_parent_as_impl, is_diag_trait_item, path_to_local, peel_ref_operators}; use if_chain::if_chain; +use rustc_ast::{FormatArgsPiece, FormatTrait}; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind, Impl, ImplItem, ImplItemKind, QPath}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; use rustc_span::{sym, symbol::kw, Symbol}; declare_clippy_lint! { @@ -89,7 +91,7 @@ declare_clippy_lint! { } #[derive(Clone, Copy)] -struct FormatTrait { +struct FormatTraitNames { /// e.g. `sym::Display` name: Symbol, /// `f` in `fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {}` @@ -99,7 +101,7 @@ struct FormatTrait { #[derive(Default)] pub struct FormatImpl { // Whether we are inside Display or Debug trait impl - None for neither - format_trait_impl: Option<FormatTrait>, + format_trait_impl: Option<FormatTraitNames>, } impl FormatImpl { @@ -161,43 +163,57 @@ fn check_to_string_in_display(cx: &LateContext<'_>, expr: &Expr<'_>) { } } -fn check_self_in_format_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, impl_trait: FormatTrait) { +fn check_self_in_format_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, impl_trait: FormatTraitNames) { // Check each arg in format calls - do we ever use Display on self (directly or via deref)? - if_chain! { - if let Some(outer_macro) = root_macro_call_first_node(cx, expr); - if let macro_def_id = outer_macro.def_id; - if let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, outer_macro.expn); - if is_format_macro(cx, macro_def_id); - then { - for arg in format_args.args { - if arg.format.r#trait != impl_trait.name { - continue; + if let Some(outer_macro) = root_macro_call_first_node(cx, expr) + && let macro_def_id = outer_macro.def_id + && is_format_macro(cx, macro_def_id) + { + find_format_args(cx, expr, outer_macro.expn, |format_args| { + for piece in &format_args.template { + if let FormatArgsPiece::Placeholder(placeholder) = piece + && let trait_name = match placeholder.format_trait { + FormatTrait::Display => sym::Display, + FormatTrait::Debug => sym::Debug, + FormatTrait::LowerExp => sym!(LowerExp), + FormatTrait::UpperExp => sym!(UpperExp), + FormatTrait::Octal => sym!(Octal), + FormatTrait::Pointer => sym::Pointer, + FormatTrait::Binary => sym!(Binary), + FormatTrait::LowerHex => sym!(LowerHex), + FormatTrait::UpperHex => sym!(UpperHex), + } + && trait_name == impl_trait.name + && let Ok(index) = placeholder.argument.index + && let Some(arg) = format_args.arguments.all_args().get(index) + && let Ok(arg_expr) = find_format_arg_expr(expr, arg) + { + check_format_arg_self(cx, expr.span, arg_expr, impl_trait); } - check_format_arg_self(cx, expr, &arg, impl_trait); } - } + }); } } -fn check_format_arg_self(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &FormatArg<'_>, impl_trait: FormatTrait) { +fn check_format_arg_self(cx: &LateContext<'_>, span: Span, arg: &Expr<'_>, impl_trait: FormatTraitNames) { // Handle multiple dereferencing of references e.g. &&self // Handle dereference of &self -> self that is equivalent (i.e. via *self in fmt() impl) // Since the argument to fmt is itself a reference: &self - let reference = peel_ref_operators(cx, arg.param.value); + let reference = peel_ref_operators(cx, arg); let map = cx.tcx.hir(); // Is the reference self? if path_to_local(reference).map(|x| map.name(x)) == Some(kw::SelfLower) { - let FormatTrait { name, .. } = impl_trait; + let FormatTraitNames { name, .. } = impl_trait; span_lint( cx, RECURSIVE_FORMAT_IMPL, - expr.span, + span, &format!("using `self` as `{name}` in `impl {name}` will cause infinite recursion"), ); } } -fn check_print_in_format_impl(cx: &LateContext<'_>, expr: &Expr<'_>, impl_trait: FormatTrait) { +fn check_print_in_format_impl(cx: &LateContext<'_>, expr: &Expr<'_>, impl_trait: FormatTraitNames) { if_chain! { if let Some(macro_call) = root_macro_call_first_node(cx, expr); if let Some(name) = cx.tcx.get_diagnostic_name(macro_call.def_id); @@ -227,7 +243,7 @@ fn check_print_in_format_impl(cx: &LateContext<'_>, expr: &Expr<'_>, impl_trait: } } -fn is_format_trait_impl(cx: &LateContext<'_>, impl_item: &ImplItem<'_>) -> Option<FormatTrait> { +fn is_format_trait_impl(cx: &LateContext<'_>, impl_item: &ImplItem<'_>) -> Option<FormatTraitNames> { if_chain! { if impl_item.ident.name == sym::fmt; if let ImplItemKind::Fn(_, body_id) = impl_item.kind; @@ -241,7 +257,7 @@ fn is_format_trait_impl(cx: &LateContext<'_>, impl_item: &ImplItem<'_>) -> Optio .and_then(|param| param.pat.simple_ident()) .map(|ident| ident.name); - Some(FormatTrait { + Some(FormatTraitNames { name, formatter_name, }) diff --git a/src/tools/clippy/clippy_lints/src/functions/must_use.rs b/src/tools/clippy/clippy_lints/src/functions/must_use.rs index 1e9e826631c..d0ad2628264 100644 --- a/src/tools/clippy/clippy_lints/src/functions/must_use.rs +++ b/src/tools/clippy/clippy_lints/src/functions/must_use.rs @@ -1,7 +1,9 @@ +use hir::FnSig; use rustc_ast::ast::Attribute; use rustc_errors::Applicability; use rustc_hir::def_id::DefIdSet; use rustc_hir::{self as hir, def::Res, QPath}; +use rustc_infer::infer::TyCtxtInferExt; use rustc_lint::{LateContext, LintContext}; use rustc_middle::{ lint::in_external_macro, @@ -27,7 +29,7 @@ pub(super) fn check_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_> let is_public = cx.effective_visibilities.is_exported(item.owner_id.def_id); let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); if let Some(attr) = attr { - check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, attr); + check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, attr, sig); } else if is_public && !is_proc_macro(attrs) && !attrs.iter().any(|a| a.has_name(sym::no_mangle)) { check_must_use_candidate( cx, @@ -49,7 +51,7 @@ pub(super) fn check_impl_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::Imp let attrs = cx.tcx.hir().attrs(item.hir_id()); let attr = cx.tcx.get_attr(item.owner_id, sym::must_use); if let Some(attr) = attr { - check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, attr); + check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, attr, sig); } else if is_public && !is_proc_macro(attrs) && trait_ref_of_method(cx, item.owner_id.def_id).is_none() { check_must_use_candidate( cx, @@ -72,7 +74,7 @@ pub(super) fn check_trait_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::Tr let attrs = cx.tcx.hir().attrs(item.hir_id()); let attr = cx.tcx.get_attr(item.owner_id, sym::must_use); if let Some(attr) = attr { - check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, attr); + check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, attr, sig); } else if let hir::TraitFn::Provided(eid) = *eid { let body = cx.tcx.hir().body(eid); if attr.is_none() && is_public && !is_proc_macro(attrs) { @@ -97,6 +99,7 @@ fn check_needless_must_use( item_span: Span, fn_header_span: Span, attr: &Attribute, + sig: &FnSig<'_>, ) { if in_external_macro(cx.sess(), item_span) { return; @@ -112,6 +115,15 @@ fn check_needless_must_use( }, ); } else if attr.value_str().is_none() && is_must_use_ty(cx, return_ty(cx, item_id)) { + // Ignore async functions unless Future::Output type is a must_use type + if sig.header.is_async() { + let infcx = cx.tcx.infer_ctxt().build(); + if let Some(future_ty) = infcx.get_impl_future_output_ty(return_ty(cx, item_id)) + && !is_must_use_ty(cx, future_ty) { + return; + } + } + span_lint_and_help( cx, DOUBLE_MUST_USE, diff --git a/src/tools/clippy/clippy_lints/src/items_after_statements.rs b/src/tools/clippy/clippy_lints/src/items_after_statements.rs index 46d439b4497..a7ec57e2850 100644 --- a/src/tools/clippy/clippy_lints/src/items_after_statements.rs +++ b/src/tools/clippy/clippy_lints/src/items_after_statements.rs @@ -1,8 +1,8 @@ //! lint when items are used after statements -use clippy_utils::diagnostics::span_lint; -use rustc_ast::ast::{Block, ItemKind, StmtKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use clippy_utils::diagnostics::span_lint_hir; +use rustc_hir::{Block, ItemKind, StmtKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_lint_pass, declare_tool_lint}; @@ -52,33 +52,34 @@ declare_clippy_lint! { declare_lint_pass!(ItemsAfterStatements => [ITEMS_AFTER_STATEMENTS]); -impl EarlyLintPass for ItemsAfterStatements { - fn check_block(&mut self, cx: &EarlyContext<'_>, item: &Block) { - if in_external_macro(cx.sess(), item.span) { +impl LateLintPass<'_> for ItemsAfterStatements { + fn check_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>) { + if in_external_macro(cx.sess(), block.span) { return; } - // skip initial items and trailing semicolons - let stmts = item + // skip initial items + let stmts = block .stmts .iter() - .map(|stmt| &stmt.kind) - .skip_while(|s| matches!(**s, StmtKind::Item(..) | StmtKind::Empty)); + .skip_while(|stmt| matches!(stmt.kind, StmtKind::Item(..))); // lint on all further items for stmt in stmts { - if let StmtKind::Item(ref it) = *stmt { - if in_external_macro(cx.sess(), it.span) { + if let StmtKind::Item(item_id) = stmt.kind { + let item = cx.tcx.hir().item(item_id); + if in_external_macro(cx.sess(), item.span) || !item.span.eq_ctxt(block.span) { return; } - if let ItemKind::MacroDef(..) = it.kind { + if let ItemKind::Macro(..) = item.kind { // do not lint `macro_rules`, but continue processing further statements continue; } - span_lint( + span_lint_hir( cx, ITEMS_AFTER_STATEMENTS, - it.span, + item.hir_id(), + item.span, "adding items after statements is confusing, since items exist from the \ start of the scope", ); diff --git a/src/tools/clippy/clippy_lints/src/large_futures.rs b/src/tools/clippy/clippy_lints/src/large_futures.rs new file mode 100644 index 00000000000..1b054481371 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/large_futures.rs @@ -0,0 +1,87 @@ +use clippy_utils::source::snippet; +use clippy_utils::{diagnostics::span_lint_and_sugg, ty::implements_trait}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, LangItem, MatchSource, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_target::abi::Size; + +declare_clippy_lint! { + /// ### What it does + /// It checks for the size of a `Future` created by `async fn` or `async {}`. + /// + /// ### Why is this bad? + /// Due to the current [unideal implemention](https://github.com/rust-lang/rust/issues/69826) of `Generator`, + /// large size of a `Future` may cause stack overflows. + /// + /// ### Example + /// ```rust + /// async fn wait(f: impl std::future::Future<Output = ()>) {} + /// + /// async fn big_fut(arg: [u8; 1024]) {} + /// + /// pub async fn test() { + /// let fut = big_fut([0u8; 1024]); + /// wait(fut).await; + /// } + /// ``` + /// + /// `Box::pin` the big future instead. + /// + /// ```rust + /// async fn wait(f: impl std::future::Future<Output = ()>) {} + /// + /// async fn big_fut(arg: [u8; 1024]) {} + /// + /// pub async fn test() { + /// let fut = Box::pin(big_fut([0u8; 1024])); + /// wait(fut).await; + /// } + /// ``` + #[clippy::version = "1.68.0"] + pub LARGE_FUTURES, + pedantic, + "large future may lead to unexpected stack overflows" +} + +#[derive(Copy, Clone)] +pub struct LargeFuture { + future_size_threshold: u64, +} + +impl LargeFuture { + pub fn new(future_size_threshold: u64) -> Self { + Self { future_size_threshold } + } +} + +impl_lint_pass!(LargeFuture => [LARGE_FUTURES]); + +impl<'tcx> LateLintPass<'tcx> for LargeFuture { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if matches!(expr.span.ctxt().outer_expn_data().kind, rustc_span::ExpnKind::Macro(..)) { + return; + } + if let ExprKind::Match(expr, _, MatchSource::AwaitDesugar) = expr.kind { + if let ExprKind::Call(func, [expr, ..]) = expr.kind + && let ExprKind::Path(QPath::LangItem(LangItem::IntoFutureIntoFuture, ..)) = func.kind + && let ty = cx.typeck_results().expr_ty(expr) + && let Some(future_trait_def_id) = cx.tcx.lang_items().future_trait() + && implements_trait(cx, ty, future_trait_def_id, &[]) + && let Ok(layout) = cx.tcx.layout_of(cx.param_env.and(ty)) + && let size = layout.layout.size() + && size >= Size::from_bytes(self.future_size_threshold) + { + span_lint_and_sugg( + cx, + LARGE_FUTURES, + expr.span, + &format!("large future with a size of {} bytes", size.bytes()), + "consider `Box::pin` on it", + format!("Box::pin({})", snippet(cx, expr.span, "..")), + Applicability::Unspecified, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/lib.rs b/src/tools/clippy/clippy_lints/src/lib.rs index 3da7f95c1b9..b0ec14855e7 100644 --- a/src/tools/clippy/clippy_lints/src/lib.rs +++ b/src/tools/clippy/clippy_lints/src/lib.rs @@ -1,7 +1,6 @@ #![feature(array_windows)] #![feature(binary_heap_into_iter_sorted)] #![feature(box_patterns)] -#![feature(drain_filter)] #![feature(if_let_guard)] #![feature(iter_intersperse)] #![feature(let_chains)] @@ -162,6 +161,7 @@ mod items_after_statements; mod iter_not_returning_iterator; mod large_const_arrays; mod large_enum_variant; +mod large_futures; mod large_include_file; mod large_stack_arrays; mod len_zero; @@ -169,6 +169,7 @@ mod let_if_seq; mod let_underscore; mod let_with_type_underscore; mod lifetimes; +mod lines_filter_map_ok; mod literal_representation; mod loops; mod macro_use; @@ -183,6 +184,7 @@ mod manual_main_separator_str; mod manual_non_exhaustive; mod manual_rem_euclid; mod manual_retain; +mod manual_slice_size_calculation; mod manual_string_new; mod manual_strip; mod map_unit_fn; @@ -281,6 +283,7 @@ mod slow_vector_initialization; mod std_instead_of_core; mod strings; mod strlen_on_c_strings; +mod suspicious_doc_comments; mod suspicious_operation_groupings; mod suspicious_trait_impl; mod suspicious_xor_used_as_pow; @@ -288,6 +291,7 @@ mod swap; mod swap_ptr_to_ref; mod tabs_in_doc_comments; mod temporary_assignment; +mod tests_outside_test_module; mod to_digit_is_some; mod trailing_empty_array; mod trait_bounds; @@ -299,6 +303,7 @@ mod uninit_vec; mod unit_return_expecting_ord; mod unit_types; mod unnamed_address; +mod unnecessary_box_returns; mod unnecessary_owned_empty_strings; mod unnecessary_self_imports; mod unnecessary_struct_initialization; @@ -344,13 +349,17 @@ pub fn register_pre_expansion_lints(store: &mut rustc_lint::LintStore, sess: &Se } #[doc(hidden)] -pub fn read_conf(sess: &Session, path: &io::Result<Option<PathBuf>>) -> Conf { +pub fn read_conf(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>)>) -> Conf { + if let Ok((_, warnings)) = path { + for warning in warnings { + sess.warn(warning); + } + } let file_name = match path { - Ok(Some(path)) => path, - Ok(None) => return Conf::default(), + Ok((Some(path), _)) => path, + Ok((None, _)) => return Conf::default(), Err(error) => { - sess.struct_err(format!("error finding Clippy's configuration file: {error}")) - .emit(); + sess.err(format!("error finding Clippy's configuration file: {error}")); return Conf::default(); }, }; @@ -746,7 +755,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_early_pass(|| Box::new(unused_unit::UnusedUnit)); store.register_late_pass(|_| Box::new(returns::Return)); store.register_early_pass(|| Box::new(collapsible_if::CollapsibleIf)); - store.register_early_pass(|| Box::new(items_after_statements::ItemsAfterStatements)); + store.register_late_pass(|_| Box::new(items_after_statements::ItemsAfterStatements)); store.register_early_pass(|| Box::new(precedence::Precedence)); store.register_late_pass(|_| Box::new(needless_parens_on_range_literals::NeedlessParensOnRangeLiterals)); store.register_early_pass(|| Box::new(needless_continue::NeedlessContinue)); @@ -808,6 +817,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(move |_| Box::new(dereference::Dereferencing::new(msrv()))); store.register_late_pass(|_| Box::new(option_if_let_else::OptionIfLetElse)); store.register_late_pass(|_| Box::new(future_not_send::FutureNotSend)); + let future_size_threshold = conf.future_size_threshold; + store.register_late_pass(move |_| Box::new(large_futures::LargeFuture::new(future_size_threshold))); store.register_late_pass(|_| Box::new(if_let_mutex::IfLetMutex)); store.register_late_pass(|_| Box::new(if_not_else::IfNotElse)); store.register_late_pass(|_| Box::new(equatable_if_let::PatternEquality)); @@ -934,11 +945,20 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|_| Box::new(no_mangle_with_rust_abi::NoMangleWithRustAbi)); store.register_late_pass(|_| Box::new(collection_is_never_read::CollectionIsNeverRead)); store.register_late_pass(|_| Box::new(missing_assert_message::MissingAssertMessage)); - store.register_early_pass(|| Box::new(redundant_async_block::RedundantAsyncBlock)); + store.register_late_pass(|_| Box::new(redundant_async_block::RedundantAsyncBlock)); store.register_late_pass(|_| Box::new(let_with_type_underscore::UnderscoreTyped)); store.register_late_pass(|_| Box::new(allow_attributes::AllowAttribute)); store.register_late_pass(move |_| Box::new(manual_main_separator_str::ManualMainSeparatorStr::new(msrv()))); store.register_late_pass(|_| Box::new(unnecessary_struct_initialization::UnnecessaryStruct)); + store.register_late_pass(move |_| { + Box::new(unnecessary_box_returns::UnnecessaryBoxReturns::new( + avoid_breaking_exported_api, + )) + }); + store.register_late_pass(|_| Box::new(lines_filter_map_ok::LinesFilterMapOk)); + store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule)); + store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation)); + store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments)); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/src/tools/clippy/clippy_lints/src/lines_filter_map_ok.rs b/src/tools/clippy/clippy_lints/src/lines_filter_map_ok.rs new file mode 100644 index 00000000000..b0f9276475d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lines_filter_map_ok.rs @@ -0,0 +1,100 @@ +use clippy_utils::{ + diagnostics::span_lint_and_then, is_diag_item_method, is_trait_method, match_def_path, path_to_local_id, paths, + ty::match_type, +}; +use rustc_errors::Applicability; +use rustc_hir::{Body, Closure, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Detect uses of `lines.filter_map(Result::ok)` or `lines.flat_map(Result::ok)` + /// when `lines` has type `std::io::Lines`. + /// + /// ### Why is this bad? + /// `Lines` instances might produce a never-ending stream of `Err`, in which case + /// `filter_map(Result::ok)` will enter an infinite loop while waiting for an + /// `Ok` variant. Calling `next()` once is sufficient to enter the infinite loop, + /// even in the absence of explicit loops in the user code. + /// + /// This situation can arise when working with user-provided paths. On some platforms, + /// `std::fs::File::open(path)` might return `Ok(fs)` even when `path` is a directory, + /// but any later attempt to read from `fs` will return an error. + /// + /// ### Known problems + /// This lint suggests replacing `filter_map()` or `flat_map()` applied to a `Lines` + /// instance in all cases. There two cases where the suggestion might not be + /// appropriate or necessary: + /// + /// - If the `Lines` instance can never produce any error, or if an error is produced + /// only once just before terminating the iterator, using `map_while()` is not + /// necessary but will not do any harm. + /// - If the `Lines` instance can produce intermittent errors then recover and produce + /// successful results, using `map_while()` would stop at the first error. + /// + /// ### Example + /// ```rust + /// # use std::{fs::File, io::{self, BufRead, BufReader}}; + /// # let _ = || -> io::Result<()> { + /// let mut lines = BufReader::new(File::open("some-path")?).lines().filter_map(Result::ok); + /// // If "some-path" points to a directory, the next statement never terminates: + /// let first_line: Option<String> = lines.next(); + /// # Ok(()) }; + /// ``` + /// Use instead: + /// ```rust + /// # use std::{fs::File, io::{self, BufRead, BufReader}}; + /// # let _ = || -> io::Result<()> { + /// let mut lines = BufReader::new(File::open("some-path")?).lines().map_while(Result::ok); + /// let first_line: Option<String> = lines.next(); + /// # Ok(()) }; + /// ``` + #[clippy::version = "1.70.0"] + pub LINES_FILTER_MAP_OK, + suspicious, + "filtering `std::io::Lines` with `filter_map()` or `flat_map()` might cause an infinite loop" +} +declare_lint_pass!(LinesFilterMapOk => [LINES_FILTER_MAP_OK]); + +impl LateLintPass<'_> for LinesFilterMapOk { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if let ExprKind::MethodCall(fm_method, fm_receiver, [fm_arg], fm_span) = expr.kind && + is_trait_method(cx, expr, sym::Iterator) && + (fm_method.ident.as_str() == "filter_map" || fm_method.ident.as_str() == "flat_map") && + match_type(cx, cx.typeck_results().expr_ty_adjusted(fm_receiver), &paths::STD_IO_LINES) + { + let lint = match &fm_arg.kind { + // Detect `Result::ok` + ExprKind::Path(qpath) => + cx.qpath_res(qpath, fm_arg.hir_id).opt_def_id().map(|did| + match_def_path(cx, did, &paths::CORE_RESULT_OK_METHOD)).unwrap_or_default(), + // Detect `|x| x.ok()` + ExprKind::Closure(Closure { body, .. }) => + if let Body { params: [param], value, .. } = cx.tcx.hir().body(*body) && + let ExprKind::MethodCall(method, receiver, [], _) = value.kind && + path_to_local_id(receiver, param.pat.hir_id) && + let Some(method_did) = cx.typeck_results().type_dependent_def_id(value.hir_id) + { + is_diag_item_method(cx, method_did, sym::Result) && method.ident.as_str() == "ok" + } else { + false + } + _ => false, + }; + if lint { + span_lint_and_then(cx, + LINES_FILTER_MAP_OK, + fm_span, + &format!("`{}()` will run forever if the iterator repeatedly produces an `Err`", fm_method.ident), + |diag| { + diag.span_note( + fm_receiver.span, + "this expression returning a `std::io::Lines` may produce an infinite number of `Err` in case of a read error"); + diag.span_suggestion(fm_span, "replace with", "map_while(Result::ok)", Applicability::MaybeIncorrect); + }); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/manual_slice_size_calculation.rs b/src/tools/clippy/clippy_lints/src/manual_slice_size_calculation.rs new file mode 100644 index 00000000000..92ee79453a3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_slice_size_calculation.rs @@ -0,0 +1,93 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{expr_or_init, in_constant}; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// When `a` is `&[T]`, detect `a.len() * size_of::<T>()` and suggest `size_of_val(a)` + /// instead. + /// + /// ### Why is this better? + /// * Shorter to write + /// * Removes the need for the human and the compiler to worry about overflow in the + /// multiplication + /// * Potentially faster at runtime as rust emits special no-wrapping flags when it + /// calculates the byte length + /// * Less turbofishing + /// + /// ### Example + /// ```rust + /// # let data : &[i32] = &[1, 2, 3]; + /// let newlen = data.len() * std::mem::size_of::<i32>(); + /// ``` + /// Use instead: + /// ```rust + /// # let data : &[i32] = &[1, 2, 3]; + /// let newlen = std::mem::size_of_val(data); + /// ``` + #[clippy::version = "1.70.0"] + pub MANUAL_SLICE_SIZE_CALCULATION, + complexity, + "manual slice size calculation" +} +declare_lint_pass!(ManualSliceSizeCalculation => [MANUAL_SLICE_SIZE_CALCULATION]); + +impl<'tcx> LateLintPass<'tcx> for ManualSliceSizeCalculation { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + // Does not apply inside const because size_of_value is not cost in stable. + if !in_constant(cx, expr.hir_id) + && let ExprKind::Binary(ref op, left, right) = expr.kind + && BinOpKind::Mul == op.node + && let Some(_receiver) = simplify(cx, left, right) + { + span_lint_and_help( + cx, + MANUAL_SLICE_SIZE_CALCULATION, + expr.span, + "manual slice size calculation", + None, + "consider using std::mem::size_of_value instead"); + } + } +} + +fn simplify<'tcx>( + cx: &LateContext<'tcx>, + expr1: &'tcx Expr<'tcx>, + expr2: &'tcx Expr<'tcx>, +) -> Option<&'tcx Expr<'tcx>> { + let expr1 = expr_or_init(cx, expr1); + let expr2 = expr_or_init(cx, expr2); + + simplify_half(cx, expr1, expr2).or_else(|| simplify_half(cx, expr2, expr1)) +} + +fn simplify_half<'tcx>( + cx: &LateContext<'tcx>, + expr1: &'tcx Expr<'tcx>, + expr2: &'tcx Expr<'tcx>, +) -> Option<&'tcx Expr<'tcx>> { + if + // expr1 is `[T1].len()`? + let ExprKind::MethodCall(method_path, receiver, _, _) = expr1.kind + && method_path.ident.name == sym::len + && let receiver_ty = cx.typeck_results().expr_ty(receiver) + && let ty::Slice(ty1) = receiver_ty.peel_refs().kind() + // expr2 is `size_of::<T2>()`? + && let ExprKind::Call(func, _) = expr2.kind + && let ExprKind::Path(ref func_qpath) = func.kind + && let Some(def_id) = cx.qpath_res(func_qpath, func.hir_id).opt_def_id() + && cx.tcx.is_diagnostic_item(sym::mem_size_of, def_id) + && let Some(ty2) = cx.typeck_results().node_substs(func.hir_id).types().next() + // T1 == T2? + && *ty1 == ty2 + { + Some(receiver) + } else { + None + } +} diff --git a/src/tools/clippy/clippy_lints/src/mem_replace.rs b/src/tools/clippy/clippy_lints/src/mem_replace.rs index 35024ec1224..8a921d4af16 100644 --- a/src/tools/clippy/clippy_lints/src/mem_replace.rs +++ b/src/tools/clippy/clippy_lints/src/mem_replace.rs @@ -1,12 +1,13 @@ use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg, span_lint_and_then}; use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::{snippet, snippet_with_applicability}; +use clippy_utils::sugg::Sugg; use clippy_utils::ty::is_non_aggregate_primitive_type; -use clippy_utils::{is_default_equivalent, is_res_lang_ctor, path_res}; +use clippy_utils::{is_default_equivalent, is_res_lang_ctor, path_res, peel_ref_operators}; use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir::LangItem::OptionNone; -use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, QPath}; +use rustc_hir::{Expr, ExprKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_tool_lint, impl_lint_pass}; @@ -101,40 +102,26 @@ declare_clippy_lint! { impl_lint_pass!(MemReplace => [MEM_REPLACE_OPTION_WITH_NONE, MEM_REPLACE_WITH_UNINIT, MEM_REPLACE_WITH_DEFAULT]); -fn check_replace_option_with_none(cx: &LateContext<'_>, src: &Expr<'_>, dest: &Expr<'_>, expr_span: Span) { - // Check that second argument is `Option::None` - if is_res_lang_ctor(cx, path_res(cx, src), OptionNone) { - // Since this is a late pass (already type-checked), - // and we already know that the second argument is an - // `Option`, we do not need to check the first - // argument's type. All that's left is to get - // replacee's path. - let replaced_path = match dest.kind { - ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, replaced) => { - if let ExprKind::Path(QPath::Resolved(None, replaced_path)) = replaced.kind { - replaced_path - } else { - return; - } - }, - ExprKind::Path(QPath::Resolved(None, replaced_path)) => replaced_path, - _ => return, - }; - - let mut applicability = Applicability::MachineApplicable; - span_lint_and_sugg( - cx, - MEM_REPLACE_OPTION_WITH_NONE, - expr_span, - "replacing an `Option` with `None`", - "consider `Option::take()` instead", - format!( - "{}.take()", - snippet_with_applicability(cx, replaced_path.span, "", &mut applicability) - ), - applicability, - ); - } +fn check_replace_option_with_none(cx: &LateContext<'_>, dest: &Expr<'_>, expr_span: Span) { + // Since this is a late pass (already type-checked), + // and we already know that the second argument is an + // `Option`, we do not need to check the first + // argument's type. All that's left is to get + // the replacee's expr after peeling off the `&mut` + let sugg_expr = peel_ref_operators(cx, dest); + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + MEM_REPLACE_OPTION_WITH_NONE, + expr_span, + "replacing an `Option` with `None`", + "consider `Option::take()` instead", + format!( + "{}.take()", + Sugg::hir_with_context(cx, sugg_expr, expr_span.ctxt(), "", &mut applicability).maybe_par() + ), + applicability, + ); } fn check_replace_with_uninit(cx: &LateContext<'_>, src: &Expr<'_>, dest: &Expr<'_>, expr_span: Span) { @@ -200,10 +187,6 @@ fn check_replace_with_default(cx: &LateContext<'_>, src: &Expr<'_>, dest: &Expr< if is_non_aggregate_primitive_type(expr_type) { return; } - // disable lint for Option since it is covered in another lint - if is_res_lang_ctor(cx, path_res(cx, src), OptionNone) { - return; - } if is_default_equivalent(cx, src) && !in_external_macro(cx.tcx.sess, expr_span) { span_lint_and_then( cx, @@ -246,11 +229,13 @@ impl<'tcx> LateLintPass<'tcx> for MemReplace { if let Some(def_id) = cx.qpath_res(func_qpath, func.hir_id).opt_def_id(); if cx.tcx.is_diagnostic_item(sym::mem_replace, def_id); then { - check_replace_option_with_none(cx, src, dest, expr.span); - check_replace_with_uninit(cx, src, dest, expr.span); - if self.msrv.meets(msrvs::MEM_TAKE) { + // Check that second argument is `Option::None` + if is_res_lang_ctor(cx, path_res(cx, src), OptionNone) { + check_replace_option_with_none(cx, dest, expr.span); + } else if self.msrv.meets(msrvs::MEM_TAKE) { check_replace_with_default(cx, src, dest, expr.span); } + check_replace_with_uninit(cx, src, dest, expr.span); } } } diff --git a/src/tools/clippy/clippy_lints/src/methods/clear_with_drain.rs b/src/tools/clippy/clippy_lints/src/methods/clear_with_drain.rs new file mode 100644 index 00000000000..67ad58d5a8c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/clear_with_drain.rs @@ -0,0 +1,53 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_range_full; +use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::{Expr, ExprKind, LangItem, QPath}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; +use rustc_span::Span; + +use super::CLEAR_WITH_DRAIN; + +// Add `String` here when it is added to diagnostic items +const ACCEPTABLE_TYPES_WITH_ARG: [rustc_span::Symbol; 2] = [sym::Vec, sym::VecDeque]; + +const ACCEPTABLE_TYPES_WITHOUT_ARG: [rustc_span::Symbol; 3] = [sym::BinaryHeap, sym::HashMap, sym::HashSet]; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span: Span, arg: Option<&Expr<'_>>) { + if let Some(arg) = arg { + if match_acceptable_type(cx, recv, &ACCEPTABLE_TYPES_WITH_ARG) + && let ExprKind::Path(QPath::Resolved(None, container_path)) = recv.kind + && is_range_full(cx, arg, Some(container_path)) + { + suggest(cx, expr, recv, span); + } + } else if match_acceptable_type(cx, recv, &ACCEPTABLE_TYPES_WITHOUT_ARG) { + suggest(cx, expr, recv, span); + } +} + +fn match_acceptable_type(cx: &LateContext<'_>, expr: &hir::Expr<'_>, types: &[rustc_span::Symbol]) -> bool { + let expr_ty = cx.typeck_results().expr_ty(expr).peel_refs(); + types.iter().any(|&ty| is_type_diagnostic_item(cx, expr_ty, ty)) + // String type is a lang item but not a diagnostic item for now so we need a separate check + || is_type_lang_item(cx, expr_ty, LangItem::String) +} + +fn suggest(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span: Span) { + if let Some(adt) = cx.typeck_results().expr_ty(recv).ty_adt_def() + // Use `opt_item_name` while `String` is not a diagnostic item + && let Some(ty_name) = cx.tcx.opt_item_name(adt.did()) + { + span_lint_and_sugg( + cx, + CLEAR_WITH_DRAIN, + span.with_hi(expr.span.hi()), + &format!("`drain` used to clear a `{ty_name}`"), + "try", + "clear()".to_string(), + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/expect_fun_call.rs b/src/tools/clippy/clippy_lints/src/methods/expect_fun_call.rs index a22285058d4..92d21bb8932 100644 --- a/src/tools/clippy/clippy_lints/src/methods/expect_fun_call.rs +++ b/src/tools/clippy/clippy_lints/src/methods/expect_fun_call.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn}; +use clippy_utils::macros::{find_format_args, format_args_inputs_span, root_macro_call_first_node}; use clippy_utils::source::snippet_with_applicability; use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item}; use rustc_errors::Applicability; @@ -136,18 +136,19 @@ pub(super) fn check<'tcx>( if !cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id) { return; } - let Some(format_args) = FormatArgsExpn::find_nested(cx, arg_root, macro_call.expn) else { return }; - let span = format_args.inputs_span(); - let sugg = snippet_with_applicability(cx, span, "..", &mut applicability); - span_lint_and_sugg( - cx, - EXPECT_FUN_CALL, - span_replace_word, - &format!("use of `{name}` followed by a function call"), - "try this", - format!("unwrap_or_else({closure_args} panic!({sugg}))"), - applicability, - ); + find_format_args(cx, arg_root, macro_call.expn, |format_args| { + let span = format_args_inputs_span(format_args); + let sugg = snippet_with_applicability(cx, span, "..", &mut applicability); + span_lint_and_sugg( + cx, + EXPECT_FUN_CALL, + span_replace_word, + &format!("use of `{name}` followed by a function call"), + "try this", + format!("unwrap_or_else({closure_args} panic!({sugg}))"), + applicability, + ); + }); return; } diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_with_drain.rs b/src/tools/clippy/clippy_lints/src/methods/iter_with_drain.rs index 3da230e12d7..f6772c5c6b3 100644 --- a/src/tools/clippy/clippy_lints/src/methods/iter_with_drain.rs +++ b/src/tools/clippy/clippy_lints/src/methods/iter_with_drain.rs @@ -1,7 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::higher::Range; -use clippy_utils::is_integer_const; -use rustc_ast::ast::RangeLimits; +use clippy_utils::is_range_full; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind, QPath}; use rustc_lint::LateContext; @@ -15,8 +13,8 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span && let Some(adt) = cx.typeck_results().expr_ty(recv).ty_adt_def() && let Some(ty_name) = cx.tcx.get_diagnostic_name(adt.did()) && matches!(ty_name, sym::Vec | sym::VecDeque) - && let Some(range) = Range::hir(arg) - && is_full_range(cx, recv, range) + && let ExprKind::Path(QPath::Resolved(None, container_path)) = recv.kind + && is_range_full(cx, arg, Some(container_path)) { span_lint_and_sugg( cx, @@ -29,19 +27,3 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span ); }; } - -fn is_full_range(cx: &LateContext<'_>, container: &Expr<'_>, range: Range<'_>) -> bool { - range.start.map_or(true, |e| is_integer_const(cx, e, 0)) - && range.end.map_or(true, |e| { - if range.limits == RangeLimits::HalfOpen - && let ExprKind::Path(QPath::Resolved(None, container_path)) = container.kind - && let ExprKind::MethodCall(name, self_arg, [], _) = e.kind - && name.ident.name == sym::len - && let ExprKind::Path(QPath::Resolved(None, path)) = self_arg.kind - { - container_path.res == path.res - } else { - false - } - }) -} diff --git a/src/tools/clippy/clippy_lints/src/methods/mod.rs b/src/tools/clippy/clippy_lints/src/methods/mod.rs index 56e3988bf09..64bf55ba24c 100644 --- a/src/tools/clippy/clippy_lints/src/methods/mod.rs +++ b/src/tools/clippy/clippy_lints/src/methods/mod.rs @@ -9,6 +9,7 @@ mod chars_last_cmp; mod chars_last_cmp_with_unwrap; mod chars_next_cmp; mod chars_next_cmp_with_unwrap; +mod clear_with_drain; mod clone_on_copy; mod clone_on_ref_ptr; mod cloned_instead_of_copied; @@ -110,7 +111,7 @@ use clippy_utils::ty::{contains_ty_adt_constructor_opaque, implements_trait, is_ use clippy_utils::{contains_return, is_bool, is_trait_method, iter_input_pats, return_ty}; use if_chain::if_chain; use rustc_hir as hir; -use rustc_hir::{Expr, ExprKind, TraitItem, TraitItemKind}; +use rustc_hir::{Expr, ExprKind, Node, Stmt, StmtKind, TraitItem, TraitItemKind}; use rustc_hir_analysis::hir_ty_to_ty; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; @@ -3190,6 +3191,31 @@ declare_clippy_lint! { "single command line argument that looks like it should be multiple arguments" } +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.drain(..)` for the sole purpose of clearing a container. + /// + /// ### Why is this bad? + /// This creates an unnecessary iterator that is dropped immediately. + /// + /// Calling `.clear()` also makes the intent clearer. + /// + /// ### Example + /// ```rust + /// let mut v = vec![1, 2, 3]; + /// v.drain(..); + /// ``` + /// Use instead: + /// ```rust + /// let mut v = vec![1, 2, 3]; + /// v.clear(); + /// ``` + #[clippy::version = "1.69.0"] + pub CLEAR_WITH_DRAIN, + nursery, + "calling `drain` in order to `clear` a container" +} + pub struct Methods { avoid_breaking_exported_api: bool, msrv: Msrv, @@ -3318,6 +3344,7 @@ impl_lint_pass!(Methods => [ SEEK_TO_START_INSTEAD_OF_REWIND, NEEDLESS_COLLECT, SUSPICIOUS_COMMAND_ARG_SPACE, + CLEAR_WITH_DRAIN, ]); /// Extracts a method call name, args, and `Span` of the method name. @@ -3562,8 +3589,15 @@ impl Methods { Some(("bytes", recv2, [], _, _)) => bytes_count_to_len::check(cx, expr, recv, recv2), _ => {}, }, - ("drain", [arg]) => { - iter_with_drain::check(cx, expr, recv, span, arg); + ("drain", ..) => { + if let Node::Stmt(Stmt { hir_id: _, kind, .. }) = cx.tcx.hir().get_parent(expr.hir_id) + && matches!(kind, StmtKind::Semi(_)) + && args.len() <= 1 + { + clear_with_drain::check(cx, expr, recv, span, args.first()); + } else if let [arg] = args { + iter_with_drain::check(cx, expr, recv, span, arg); + } }, ("ends_with", [arg]) => { if let ExprKind::MethodCall(.., span) = expr.kind { diff --git a/src/tools/clippy/clippy_lints/src/missing_const_for_fn.rs b/src/tools/clippy/clippy_lints/src/missing_const_for_fn.rs index 87bd007a26a..f1831a30461 100644 --- a/src/tools/clippy/clippy_lints/src/missing_const_for_fn.rs +++ b/src/tools/clippy/clippy_lints/src/missing_const_for_fn.rs @@ -41,6 +41,7 @@ declare_clippy_lint! { /// can't be const as it calls a non-const function. Making `a` const and running Clippy again, /// will suggest to make `b` const, too. /// + /// If you are marking a public function with `const`, removing it again will break API compatibility. /// ### Example /// ```rust /// # struct Foo { diff --git a/src/tools/clippy/clippy_lints/src/operators/arithmetic_side_effects.rs b/src/tools/clippy/clippy_lints/src/operators/arithmetic_side_effects.rs index 25e8de94863..e5713735672 100644 --- a/src/tools/clippy/clippy_lints/src/operators/arithmetic_side_effects.rs +++ b/src/tools/clippy/clippy_lints/src/operators/arithmetic_side_effects.rs @@ -154,10 +154,18 @@ impl ArithmeticSideEffects { Self::literal_integer(cx, actual_rhs), ) { (None, None) => false, - (None, Some(n)) | (Some(n), None) => match (&op.node, n) { + (None, Some(n)) => match (&op.node, n) { // Division and module are always valid if applied to non-zero integers (hir::BinOpKind::Div | hir::BinOpKind::Rem, local_n) if local_n != 0 => true, - // Addition or subtracting zeros is always a no-op + // Adding or subtracting zeros is always a no-op + (hir::BinOpKind::Add | hir::BinOpKind::Sub, 0) + // Multiplication by 1 or 0 will never overflow + | (hir::BinOpKind::Mul, 0 | 1) + => true, + _ => false, + }, + (Some(n), None) => match (&op.node, n) { + // Adding or subtracting zeros is always a no-op (hir::BinOpKind::Add | hir::BinOpKind::Sub, 0) // Multiplication by 1 or 0 will never overflow | (hir::BinOpKind::Mul, 0 | 1) diff --git a/src/tools/clippy/clippy_lints/src/redundant_async_block.rs b/src/tools/clippy/clippy_lints/src/redundant_async_block.rs index 5ac203665d0..a0f831764d0 100644 --- a/src/tools/clippy/clippy_lints/src/redundant_async_block.rs +++ b/src/tools/clippy/clippy_lints/src/redundant_async_block.rs @@ -1,8 +1,15 @@ -use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet}; -use rustc_ast::ast::{Expr, ExprKind, Stmt, StmtKind}; -use rustc_ast::visit::Visitor as AstVisitor; +use std::ops::ControlFlow; + +use clippy_utils::{ + diagnostics::span_lint_and_sugg, + peel_blocks, + source::{snippet, walk_span_to_context}, + visitors::for_each_expr, +}; use rustc_errors::Applicability; -use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_hir::{AsyncGeneratorKind, Closure, Expr, ExprKind, GeneratorKind, MatchSource}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::{lint::in_external_macro, ty::UpvarCapture}; use rustc_session::{declare_lint_pass, declare_tool_lint}; declare_clippy_lint! { @@ -14,106 +21,88 @@ declare_clippy_lint! { /// /// ### Example /// ```rust - /// async fn f() -> i32 { - /// 1 + 2 - /// } - /// + /// let f = async { + /// 1 + 2 + /// }; /// let fut = async { - /// f().await + /// f.await /// }; /// ``` /// Use instead: /// ```rust - /// async fn f() -> i32 { - /// 1 + 2 - /// } - /// - /// let fut = f(); + /// let f = async { + /// 1 + 2 + /// }; + /// let fut = f; /// ``` #[clippy::version = "1.69.0"] pub REDUNDANT_ASYNC_BLOCK, - nursery, + complexity, "`async { future.await }` can be replaced by `future`" } declare_lint_pass!(RedundantAsyncBlock => [REDUNDANT_ASYNC_BLOCK]); -impl EarlyLintPass for RedundantAsyncBlock { - fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { - if expr.span.from_expansion() { - return; - } - if let ExprKind::Async(_, block) = &expr.kind && block.stmts.len() == 1 && - let Some(Stmt { kind: StmtKind::Expr(last), .. }) = block.stmts.last() && - let ExprKind::Await(future) = &last.kind && - !future.span.from_expansion() && - !await_in_expr(future) +impl<'tcx> LateLintPass<'tcx> for RedundantAsyncBlock { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let span = expr.span; + if !in_external_macro(cx.tcx.sess, span) && + let Some(body_expr) = desugar_async_block(cx, expr) && + let Some(expr) = desugar_await(peel_blocks(body_expr)) && + // The await prefix must not come from a macro as its content could change in the future. + expr.span.ctxt() == body_expr.span.ctxt() && + // An async block does not have immediate side-effects from a `.await` point-of-view. + (!expr.can_have_side_effects() || desugar_async_block(cx, expr).is_some()) && + let Some(shortened_span) = walk_span_to_context(expr.span, span.ctxt()) { - if captures_value(last) { - // If the async block captures variables then there is no equivalence. - return; - } - span_lint_and_sugg( cx, REDUNDANT_ASYNC_BLOCK, - expr.span, + span, "this async expression only awaits a single future", "you can reduce it to", - snippet(cx, future.span, "..").into_owned(), + snippet(cx, shortened_span, "..").into_owned(), Applicability::MachineApplicable, ); } } } -/// Check whether an expression contains `.await` -fn await_in_expr(expr: &Expr) -> bool { - let mut detector = AwaitDetector::default(); - detector.visit_expr(expr); - detector.await_found -} - -#[derive(Default)] -struct AwaitDetector { - await_found: bool, -} - -impl<'ast> AstVisitor<'ast> for AwaitDetector { - fn visit_expr(&mut self, ex: &'ast Expr) { - match (&ex.kind, self.await_found) { - (ExprKind::Await(_), _) => self.await_found = true, - (_, false) => rustc_ast::visit::walk_expr(self, ex), - _ => (), - } +/// If `expr` is a desugared `async` block, return the original expression if it does not capture +/// any variable by ref. +fn desugar_async_block<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { + if let ExprKind::Closure(Closure { body, def_id, .. }) = expr.kind && + let body = cx.tcx.hir().body(*body) && + matches!(body.generator_kind, Some(GeneratorKind::Async(AsyncGeneratorKind::Block))) + { + cx + .typeck_results() + .closure_min_captures + .get(def_id) + .map_or(true, |m| { + m.values().all(|places| { + places + .iter() + .all(|place| matches!(place.info.capture_kind, UpvarCapture::ByValue)) + }) + }) + .then_some(body.value) + } else { + None } } -/// Check whether an expression may have captured a local variable. -/// This is done by looking for paths with only one segment, except as -/// a prefix of `.await` since this would be captured by value. -/// -/// This function will sometimes return `true` even tough there are no -/// captures happening: at the AST level, it is impossible to -/// dinstinguish a function call from a call to a closure which comes -/// from the local environment. -fn captures_value(expr: &Expr) -> bool { - let mut detector = CaptureDetector::default(); - detector.visit_expr(expr); - detector.capture_found -} - -#[derive(Default)] -struct CaptureDetector { - capture_found: bool, -} - -impl<'ast> AstVisitor<'ast> for CaptureDetector { - fn visit_expr(&mut self, ex: &'ast Expr) { - match (&ex.kind, self.capture_found) { - (ExprKind::Await(fut), _) if matches!(fut.kind, ExprKind::Path(..)) => (), - (ExprKind::Path(_, path), _) if path.segments.len() == 1 => self.capture_found = true, - (_, false) => rustc_ast::visit::walk_expr(self, ex), - _ => (), - } +/// If `expr` is a desugared `.await`, return the original expression if it does not come from a +/// macro expansion. +fn desugar_await<'tcx>(expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { + if let ExprKind::Match(match_value, _, MatchSource::AwaitDesugar) = expr.kind && + let ExprKind::Call(_, [into_future_arg]) = match_value.kind && + let ctxt = expr.span.ctxt() && + for_each_expr(into_future_arg, |e| + walk_span_to_context(e.span, ctxt) + .map_or(ControlFlow::Break(()), |_| ControlFlow::Continue(()))).is_none() + { + Some(into_future_arg) + } else { + None } } diff --git a/src/tools/clippy/clippy_lints/src/redundant_static_lifetimes.rs b/src/tools/clippy/clippy_lints/src/redundant_static_lifetimes.rs index 11b908e7e53..038dfe8e480 100644 --- a/src/tools/clippy/clippy_lints/src/redundant_static_lifetimes.rs +++ b/src/tools/clippy/clippy_lints/src/redundant_static_lifetimes.rs @@ -1,7 +1,7 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::snippet; -use rustc_ast::ast::{Item, ItemKind, Ty, TyKind, StaticItem, ConstItem}; +use rustc_ast::ast::{ConstItem, Item, ItemKind, StaticItem, Ty, TyKind}; use rustc_errors::Applicability; use rustc_lint::{EarlyContext, EarlyLintPass}; use rustc_session::{declare_tool_lint, impl_lint_pass}; @@ -106,7 +106,7 @@ impl EarlyLintPass for RedundantStaticLifetimes { // #2438) } - if let ItemKind::Static(box StaticItem { ty: ref var_type,.. }) = item.kind { + if let ItemKind::Static(box StaticItem { ty: ref var_type, .. }) = item.kind { Self::visit_type(var_type, cx, "statics have by default a `'static` lifetime"); } } diff --git a/src/tools/clippy/clippy_lints/src/returns.rs b/src/tools/clippy/clippy_lints/src/returns.rs index f0d7dd23a67..df126d7617e 100644 --- a/src/tools/clippy/clippy_lints/src/returns.rs +++ b/src/tools/clippy/clippy_lints/src/returns.rs @@ -9,7 +9,7 @@ use rustc_hir::intravisit::FnKind; use rustc_hir::{Block, Body, Expr, ExprKind, FnDecl, LangItem, MatchSource, PatKind, QPath, StmtKind}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; -use rustc_middle::ty::subst::GenericArgKind; +use rustc_middle::ty::{self, subst::GenericArgKind, Ty}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::def_id::LocalDefId; use rustc_span::source_map::Span; @@ -175,7 +175,7 @@ impl<'tcx> LateLintPass<'tcx> for Return { } else { RetReplacement::Empty }; - check_final_expr(cx, body.value, vec![], replacement); + check_final_expr(cx, body.value, vec![], replacement, None); }, FnKind::ItemFn(..) | FnKind::Method(..) => { check_block_return(cx, &body.value.kind, sp, vec![]); @@ -188,11 +188,11 @@ impl<'tcx> LateLintPass<'tcx> for Return { fn check_block_return<'tcx>(cx: &LateContext<'tcx>, expr_kind: &ExprKind<'tcx>, sp: Span, mut semi_spans: Vec<Span>) { if let ExprKind::Block(block, _) = expr_kind { if let Some(block_expr) = block.expr { - check_final_expr(cx, block_expr, semi_spans, RetReplacement::Empty); + check_final_expr(cx, block_expr, semi_spans, RetReplacement::Empty, None); } else if let Some(stmt) = block.stmts.iter().last() { match stmt.kind { StmtKind::Expr(expr) => { - check_final_expr(cx, expr, semi_spans, RetReplacement::Empty); + check_final_expr(cx, expr, semi_spans, RetReplacement::Empty, None); }, StmtKind::Semi(semi_expr) => { // Remove ending semicolons and any whitespace ' ' in between. @@ -202,7 +202,7 @@ fn check_block_return<'tcx>(cx: &LateContext<'tcx>, expr_kind: &ExprKind<'tcx>, span_find_starting_semi(cx.sess().source_map(), semi_span.with_hi(sp.hi())); semi_spans.push(semi_span_to_remove); } - check_final_expr(cx, semi_expr, semi_spans, RetReplacement::Empty); + check_final_expr(cx, semi_expr, semi_spans, RetReplacement::Empty, None); }, _ => (), } @@ -216,6 +216,7 @@ fn check_final_expr<'tcx>( semi_spans: Vec<Span>, /* containing all the places where we would need to remove semicolons if finding an * needless return */ replacement: RetReplacement<'tcx>, + match_ty_opt: Option<Ty<'_>>, ) { let peeled_drop_expr = expr.peel_drop_temps(); match &peeled_drop_expr.kind { @@ -244,7 +245,22 @@ fn check_final_expr<'tcx>( RetReplacement::Expr(snippet, applicability) } } else { - replacement + match match_ty_opt { + Some(match_ty) => { + match match_ty.kind() { + // If the code got till here with + // tuple not getting detected before it, + // then we are sure it's going to be Unit + // type + ty::Tuple(_) => RetReplacement::Unit, + // We don't want to anything in this case + // cause we can't predict what the user would + // want here + _ => return, + } + }, + None => replacement, + } }; if !cx.tcx.hir().attrs(expr.hir_id).is_empty() { @@ -268,8 +284,9 @@ fn check_final_expr<'tcx>( // note, if without else is going to be a type checking error anyways // (except for unit type functions) so we don't match it ExprKind::Match(_, arms, MatchSource::Normal) => { + let match_ty = cx.typeck_results().expr_ty(peeled_drop_expr); for arm in arms.iter() { - check_final_expr(cx, arm.body, semi_spans.clone(), RetReplacement::Unit); + check_final_expr(cx, arm.body, semi_spans.clone(), RetReplacement::Unit, Some(match_ty)); } }, // if it's a whole block, check it @@ -293,6 +310,7 @@ fn emit_return_lint(cx: &LateContext<'_>, ret_span: Span, semi_spans: Vec<Span>, if ret_span.from_expansion() { return; } + let applicability = replacement.applicability().unwrap_or(Applicability::MachineApplicable); let return_replacement = replacement.to_string(); let sugg_help = replacement.sugg_help(); diff --git a/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs b/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs index d46f6a6352c..5743dd21c28 100644 --- a/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs +++ b/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs @@ -1,6 +1,7 @@ use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; use rustc_ast::node_id::{NodeId, NodeMap}; -use rustc_ast::{ptr::P, Crate, Item, ItemKind, MacroDef, ModKind, UseTreeKind}; +use rustc_ast::visit::{walk_expr, Visitor}; +use rustc_ast::{ptr::P, Crate, Expr, ExprKind, Item, ItemKind, MacroDef, ModKind, Ty, TyKind, UseTreeKind}; use rustc_errors::Applicability; use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; use rustc_session::{declare_tool_lint, impl_lint_pass}; @@ -55,7 +56,7 @@ impl EarlyLintPass for SingleComponentPathImports { return; } - self.check_mod(cx, &krate.items); + self.check_mod(&krate.items); } fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { @@ -84,8 +85,43 @@ impl EarlyLintPass for SingleComponentPathImports { } } +#[derive(Default)] +struct ImportUsageVisitor { + // keep track of imports reused with `self` keyword, such as `self::std` in the example below. + // Removing the `use std;` would make this a compile error (#10549) + // ``` + // use std; + // + // fn main() { + // let _ = self::std::io::stdout(); + // } + // ``` + imports_referenced_with_self: Vec<Symbol>, +} + +impl<'tcx> Visitor<'tcx> for ImportUsageVisitor { + fn visit_expr(&mut self, expr: &Expr) { + if let ExprKind::Path(_, path) = &expr.kind + && path.segments.len() > 1 + && path.segments[0].ident.name == kw::SelfLower + { + self.imports_referenced_with_self.push(path.segments[1].ident.name); + } + walk_expr(self, expr); + } + + fn visit_ty(&mut self, ty: &Ty) { + if let TyKind::Path(_, path) = &ty.kind + && path.segments.len() > 1 + && path.segments[0].ident.name == kw::SelfLower + { + self.imports_referenced_with_self.push(path.segments[1].ident.name); + } + } +} + impl SingleComponentPathImports { - fn check_mod(&mut self, cx: &EarlyContext<'_>, items: &[P<Item>]) { + fn check_mod(&mut self, items: &[P<Item>]) { // keep track of imports reused with `self` keyword, such as `self::crypto_hash` in the example // below. Removing the `use crypto_hash;` would make this a compile error // ``` @@ -108,18 +144,16 @@ impl SingleComponentPathImports { // ``` let mut macros = Vec::new(); + let mut import_usage_visitor = ImportUsageVisitor::default(); for item in items { - self.track_uses( - cx, - item, - &mut imports_reused_with_self, - &mut single_use_usages, - &mut macros, - ); + self.track_uses(item, &mut imports_reused_with_self, &mut single_use_usages, &mut macros); + import_usage_visitor.visit_item(item); } for usage in single_use_usages { - if !imports_reused_with_self.contains(&usage.name) { + if !imports_reused_with_self.contains(&usage.name) + && !import_usage_visitor.imports_referenced_with_self.contains(&usage.name) + { self.found.entry(usage.item_id).or_default().push(usage); } } @@ -127,7 +161,6 @@ impl SingleComponentPathImports { fn track_uses( &mut self, - cx: &EarlyContext<'_>, item: &Item, imports_reused_with_self: &mut Vec<Symbol>, single_use_usages: &mut Vec<SingleUse>, @@ -139,7 +172,7 @@ impl SingleComponentPathImports { match &item.kind { ItemKind::Mod(_, ModKind::Loaded(ref items, ..)) => { - self.check_mod(cx, items); + self.check_mod(items); }, ItemKind::MacroDef(MacroDef { macro_rules: true, .. }) => { macros.push(item.ident.name); diff --git a/src/tools/clippy/clippy_lints/src/suspicious_doc_comments.rs b/src/tools/clippy/clippy_lints/src/suspicious_doc_comments.rs new file mode 100644 index 00000000000..e5746ca99ca --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/suspicious_doc_comments.rs @@ -0,0 +1,94 @@ +use clippy_utils::diagnostics::{multispan_sugg_with_applicability, span_lint_and_then}; +use if_chain::if_chain; +use rustc_ast::{token::CommentKind, AttrKind, AttrStyle, Attribute, Item}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Detects the use of outer doc comments (`///`, `/**`) followed by a bang (`!`): `///!` + /// + /// ### Why is this bad? + /// Triple-slash comments (known as "outer doc comments") apply to items that follow it. + /// An outer doc comment followed by a bang (i.e. `///!`) has no specific meaning. + /// + /// The user most likely meant to write an inner doc comment (`//!`, `/*!`), which + /// applies to the parent item (i.e. the item that the comment is contained in, + /// usually a module or crate). + /// + /// ### Known problems + /// Inner doc comments can only appear before items, so there are certain cases where the suggestion + /// made by this lint is not valid code. For example: + /// ```rs + /// fn foo() {} + /// ///! + /// fn bar() {} + /// ``` + /// This lint detects the doc comment and suggests changing it to `//!`, but an inner doc comment + /// is not valid at that position. + /// + /// ### Example + /// In this example, the doc comment is attached to the *function*, rather than the *module*. + /// ```rust + /// pub mod util { + /// ///! This module contains utility functions. + /// + /// pub fn dummy() {} + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// pub mod util { + /// //! This module contains utility functions. + /// + /// pub fn dummy() {} + /// } + /// ``` + #[clippy::version = "1.70.0"] + pub SUSPICIOUS_DOC_COMMENTS, + suspicious, + "suspicious usage of (outer) doc comments" +} +declare_lint_pass!(SuspiciousDocComments => [SUSPICIOUS_DOC_COMMENTS]); + +const WARNING: &str = "this is an outer doc comment and does not apply to the parent module or crate"; +const HELP: &str = "use an inner doc comment to document the parent module or crate"; + +impl EarlyLintPass for SuspiciousDocComments { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + let replacements = collect_doc_comment_replacements(&item.attrs); + + if let Some(((lo_span, _), (hi_span, _))) = replacements.first().zip(replacements.last()) { + let span = lo_span.to(*hi_span); + + span_lint_and_then(cx, SUSPICIOUS_DOC_COMMENTS, span, WARNING, |diag| { + multispan_sugg_with_applicability(diag, HELP, Applicability::MaybeIncorrect, replacements); + }); + } + } +} + +fn collect_doc_comment_replacements(attrs: &[Attribute]) -> Vec<(Span, String)> { + attrs + .iter() + .filter_map(|attr| { + if_chain! { + if let AttrKind::DocComment(com_kind, sym) = attr.kind; + if let AttrStyle::Outer = attr.style; + if let Some(com) = sym.as_str().strip_prefix('!'); + then { + let sugg = match com_kind { + CommentKind::Line => format!("//!{com}"), + CommentKind::Block => format!("/*!{com}*/") + }; + Some((attr.span, sugg)) + } else { + None + } + } + }) + .collect() +} diff --git a/src/tools/clippy/clippy_lints/src/tests_outside_test_module.rs b/src/tools/clippy/clippy_lints/src/tests_outside_test_module.rs new file mode 100644 index 00000000000..0a0a77082e0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/tests_outside_test_module.rs @@ -0,0 +1,71 @@ +use clippy_utils::{diagnostics::span_lint_and_note, is_in_cfg_test, is_in_test_function}; +use rustc_hir::{intravisit::FnKind, Body, FnDecl}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{def_id::LocalDefId, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Triggers when a testing function (marked with the `#[test]` attribute) isn't inside a testing module + /// (marked with `#[cfg(test)]`). + /// ### Why is this bad? + /// The idiomatic (and more performant) way of writing tests is inside a testing module (flagged with `#[cfg(test)]`), + /// having test functions outside of this module is confusing and may lead to them being "hidden". + /// ### Example + /// ```rust + /// #[test] + /// fn my_cool_test() { + /// // [...] + /// } + /// + /// #[cfg(test)] + /// mod tests { + /// // [...] + /// } + /// + /// ``` + /// Use instead: + /// ```rust + /// #[cfg(test)] + /// mod tests { + /// #[test] + /// fn my_cool_test() { + /// // [...] + /// } + /// } + /// ``` + #[clippy::version = "1.70.0"] + pub TESTS_OUTSIDE_TEST_MODULE, + restriction, + "A test function is outside the testing module." +} + +declare_lint_pass!(TestsOutsideTestModule => [TESTS_OUTSIDE_TEST_MODULE]); + +impl LateLintPass<'_> for TestsOutsideTestModule { + fn check_fn( + &mut self, + cx: &LateContext<'_>, + kind: FnKind<'_>, + _: &FnDecl<'_>, + body: &Body<'_>, + sp: Span, + _: LocalDefId, + ) { + if_chain! { + if !matches!(kind, FnKind::Closure); + if is_in_test_function(cx.tcx, body.id().hir_id); + if !is_in_cfg_test(cx.tcx, body.id().hir_id); + then { + span_lint_and_note( + cx, + TESTS_OUTSIDE_TEST_MODULE, + sp, + "this function marked with #[test] is outside a #[cfg(test)] module", + None, + "move it to a testing module marked with #[cfg(test)]", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmutes_expressible_as_ptr_casts.rs b/src/tools/clippy/clippy_lints/src/transmute/transmutes_expressible_as_ptr_casts.rs index 8530b43243f..85cd74f23ef 100644 --- a/src/tools/clippy/clippy_lints/src/transmute/transmutes_expressible_as_ptr_casts.rs +++ b/src/tools/clippy/clippy_lints/src/transmute/transmutes_expressible_as_ptr_casts.rs @@ -2,8 +2,9 @@ use super::utils::check_cast; use super::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS; use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::sugg::Sugg; +use rustc_ast::ExprPrecedence; use rustc_errors::Applicability; -use rustc_hir::Expr; +use rustc_hir::{Expr, Node}; use rustc_lint::LateContext; use rustc_middle::ty::{cast::CastKind, Ty}; @@ -19,7 +20,7 @@ pub(super) fn check<'tcx>( ) -> bool { use CastKind::{AddrPtrCast, ArrayPtrCast, FnPtrAddrCast, FnPtrPtrCast, PtrAddrCast, PtrPtrCast}; let mut app = Applicability::MachineApplicable; - let sugg = match check_cast(cx, e, from_ty, to_ty) { + let mut sugg = match check_cast(cx, e, from_ty, to_ty) { Some(PtrPtrCast | AddrPtrCast | ArrayPtrCast | FnPtrPtrCast | FnPtrAddrCast) => { Sugg::hir_with_context(cx, arg, e.span.ctxt(), "..", &mut app) .as_ty(to_ty.to_string()) @@ -39,6 +40,12 @@ pub(super) fn check<'tcx>( _ => return false, }; + if let Node::Expr(parent) = cx.tcx.hir().get_parent(e.hir_id) + && parent.precedence().order() > ExprPrecedence::Cast.order() + { + sugg = format!("({sugg})"); + } + span_lint_and_sugg( cx, TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS, diff --git a/src/tools/clippy/clippy_lints/src/unnecessary_box_returns.rs b/src/tools/clippy/clippy_lints/src/unnecessary_box_returns.rs new file mode 100644 index 00000000000..912bcda630b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unnecessary_box_returns.rs @@ -0,0 +1,120 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use rustc_errors::Applicability; +use rustc_hir::{def_id::LocalDefId, FnDecl, FnRetTy, ImplItemKind, Item, ItemKind, Node, TraitItem, TraitItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Symbol; + +declare_clippy_lint! { + /// ### What it does + /// + /// Checks for a return type containing a `Box<T>` where `T` implements `Sized` + /// + /// ### Why is this bad? + /// + /// It's better to just return `T` in these cases. The caller may not need + /// the value to be boxed, and it's expensive to free the memory once the + /// `Box<T>` been dropped. + /// + /// ### Example + /// ```rust + /// fn foo() -> Box<String> { + /// Box::new(String::from("Hello, world!")) + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn foo() -> String { + /// String::from("Hello, world!") + /// } + /// ``` + #[clippy::version = "1.70.0"] + pub UNNECESSARY_BOX_RETURNS, + pedantic, + "Needlessly returning a Box" +} + +pub struct UnnecessaryBoxReturns { + avoid_breaking_exported_api: bool, +} + +impl_lint_pass!(UnnecessaryBoxReturns => [UNNECESSARY_BOX_RETURNS]); + +impl UnnecessaryBoxReturns { + pub fn new(avoid_breaking_exported_api: bool) -> Self { + Self { + avoid_breaking_exported_api, + } + } + + fn check_fn_item(&mut self, cx: &LateContext<'_>, decl: &FnDecl<'_>, def_id: LocalDefId, name: Symbol) { + // we don't want to tell someone to break an exported function if they ask us not to + if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(def_id) { + return; + } + + // functions which contain the word "box" are exempt from this lint + if name.as_str().contains("box") { + return; + } + + let FnRetTy::Return(return_ty_hir) = &decl.output else { return }; + + let return_ty = cx + .tcx + .erase_late_bound_regions(cx.tcx.fn_sig(def_id).skip_binder()) + .output(); + + if !return_ty.is_box() { + return; + } + + let boxed_ty = return_ty.boxed_ty(); + + // it's sometimes useful to return Box<T> if T is unsized, so don't lint those + if boxed_ty.is_sized(cx.tcx, cx.param_env) { + span_lint_and_then( + cx, + UNNECESSARY_BOX_RETURNS, + return_ty_hir.span, + format!("boxed return of the sized type `{boxed_ty}`").as_str(), + |diagnostic| { + diagnostic.span_suggestion( + return_ty_hir.span, + "try", + boxed_ty.to_string(), + // the return value and function callers also needs to + // be changed, so this can't be MachineApplicable + Applicability::Unspecified, + ); + diagnostic.help("changing this also requires a change to the return expressions in this function"); + }, + ); + } + } +} + +impl LateLintPass<'_> for UnnecessaryBoxReturns { + fn check_trait_item(&mut self, cx: &LateContext<'_>, item: &TraitItem<'_>) { + let TraitItemKind::Fn(signature, _) = &item.kind else { return }; + self.check_fn_item(cx, signature.decl, item.owner_id.def_id, item.ident.name); + } + + fn check_impl_item(&mut self, cx: &LateContext<'_>, item: &rustc_hir::ImplItem<'_>) { + // Ignore implementations of traits, because the lint should be on the + // trait, not on the implmentation of it. + let Node::Item(parent) = cx.tcx.hir().get_parent(item.hir_id()) else { return }; + let ItemKind::Impl(parent) = parent.kind else { return }; + if parent.of_trait.is_some() { + return; + } + + let ImplItemKind::Fn(signature, ..) = &item.kind else { return }; + self.check_fn_item(cx, signature.decl, item.owner_id.def_id, item.ident.name); + } + + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + let ItemKind::Fn(signature, ..) = &item.kind else { return }; + self.check_fn_item(cx, signature.decl, item.owner_id.def_id, item.ident.name); + } +} diff --git a/src/tools/clippy/clippy_lints/src/unnecessary_struct_initialization.rs b/src/tools/clippy/clippy_lints/src/unnecessary_struct_initialization.rs index af0b4b1592f..084b031982d 100644 --- a/src/tools/clippy/clippy_lints/src/unnecessary_struct_initialization.rs +++ b/src/tools/clippy/clippy_lints/src/unnecessary_struct_initialization.rs @@ -9,7 +9,7 @@ declare_clippy_lint! { /// any field. /// /// ### Why is this bad? - /// Readibility suffers from unnecessary struct building. + /// Readability suffers from unnecessary struct building. /// /// ### Example /// ```rust @@ -25,9 +25,13 @@ declare_clippy_lint! { /// let a = S { s: String::from("Hello, world!") }; /// let b = a; /// ``` + /// + /// ### Known Problems + /// Has false positives when the base is a place expression that cannot be + /// moved out of, see [#10547](https://github.com/rust-lang/rust-clippy/issues/10547). #[clippy::version = "1.70.0"] pub UNNECESSARY_STRUCT_INITIALIZATION, - complexity, + nursery, "struct built from a base that can be written mode concisely" } declare_lint_pass!(UnnecessaryStruct => [UNNECESSARY_STRUCT_INITIALIZATION]); diff --git a/src/tools/clippy/clippy_lints/src/use_self.rs b/src/tools/clippy/clippy_lints/src/use_self.rs index 7dfb0956077..5a02987453c 100644 --- a/src/tools/clippy/clippy_lints/src/use_self.rs +++ b/src/tools/clippy/clippy_lints/src/use_self.rs @@ -10,8 +10,8 @@ use rustc_hir::{ def::{CtorOf, DefKind, Res}, def_id::LocalDefId, intravisit::{walk_inf, walk_ty, Visitor}, - Expr, ExprKind, FnRetTy, FnSig, GenericArg, GenericArgsParentheses, GenericParam, GenericParamKind, HirId, Impl, ImplItemKind, Item, - ItemKind, Pat, PatKind, Path, QPath, Ty, TyKind, + Expr, ExprKind, FnRetTy, FnSig, GenericArg, GenericArgsParentheses, GenericParam, GenericParamKind, HirId, Impl, + ImplItemKind, Item, ItemKind, Pat, PatKind, Path, QPath, Ty, TyKind, }; use rustc_hir_analysis::hir_ty_to_ty; use rustc_lint::{LateContext, LateLintPass}; diff --git a/src/tools/clippy/clippy_lints/src/utils/conf.rs b/src/tools/clippy/clippy_lints/src/utils/conf.rs index 8ba252425a3..896a01af37d 100644 --- a/src/tools/clippy/clippy_lints/src/utils/conf.rs +++ b/src/tools/clippy/clippy_lints/src/utils/conf.rs @@ -249,7 +249,7 @@ define_Conf! { /// arithmetic-side-effects-allowed-unary = ["SomeType", "AnotherType"] /// ``` (arithmetic_side_effects_allowed_unary: rustc_data_structures::fx::FxHashSet<String> = <_>::default()), - /// Lint: ENUM_VARIANT_NAMES, LARGE_TYPES_PASSED_BY_VALUE, TRIVIALLY_COPY_PASS_BY_REF, UNNECESSARY_WRAPS, UNUSED_SELF, UPPER_CASE_ACRONYMS, WRONG_SELF_CONVENTION, BOX_COLLECTION, REDUNDANT_ALLOCATION, RC_BUFFER, VEC_BOX, OPTION_OPTION, LINKEDLIST, RC_MUTEX. + /// Lint: ENUM_VARIANT_NAMES, LARGE_TYPES_PASSED_BY_VALUE, TRIVIALLY_COPY_PASS_BY_REF, UNNECESSARY_WRAPS, UNUSED_SELF, UPPER_CASE_ACRONYMS, WRONG_SELF_CONVENTION, BOX_COLLECTION, REDUNDANT_ALLOCATION, RC_BUFFER, VEC_BOX, OPTION_OPTION, LINKEDLIST, RC_MUTEX, UNNECESSARY_BOX_RETURNS. /// /// Suppress lints whenever the suggested change would cause breakage for other crates. (avoid_breaking_exported_api: bool = true), @@ -275,13 +275,13 @@ define_Conf! { /// /// The list of disallowed names to lint about. NB: `bar` is not here since it has legitimate uses. The value /// `".."` can be used as part of the list to indicate, that the configured values should be appended to the - /// default configuration of Clippy. By default any configuration will replace the default value. + /// default configuration of Clippy. By default, any configuration will replace the default value. (disallowed_names: Vec<String> = super::DEFAULT_DISALLOWED_NAMES.iter().map(ToString::to_string).collect()), /// Lint: DOC_MARKDOWN. /// /// The list of words this lint should not consider as identifiers needing ticks. The value /// `".."` can be used as part of the list to indicate, that the configured values should be appended to the - /// default configuration of Clippy. By default any configuraction will replace the default value. For example: + /// default configuration of Clippy. By default, any configuration will replace the default value. For example: /// * `doc-valid-idents = ["ClipPy"]` would replace the default list with `["ClipPy"]`. /// * `doc-valid-idents = ["ClipPy", ".."]` would append `ClipPy` to the default list. /// @@ -390,7 +390,7 @@ define_Conf! { /// Enforce the named macros always use the braces specified. /// /// A `MacroMatcher` can be added like so `{ name = "macro_name", brace = "(" }`. If the macro - /// is could be used with a full path two `MacroMatcher`s have to be added one with the full path + /// could be used with a full path two `MacroMatcher`s have to be added one with the full path /// `crate_name::macro_name` and one with just the macro name. (standard_macro_braces: Vec<crate::nonstandard_macro_braces::MacroMatcher> = Vec::new()), /// Lint: MISSING_ENFORCED_IMPORT_RENAMES. @@ -408,7 +408,7 @@ define_Conf! { /// Lint: INDEX_REFUTABLE_SLICE. /// /// When Clippy suggests using a slice pattern, this is the maximum number of elements allowed in - /// the slice pattern that is suggested. If more elements would be necessary, the lint is suppressed. + /// the slice pattern that is suggested. If more elements are necessary, the lint is suppressed. /// For example, `[_, _, _, e, ..]` is a slice pattern with 4 elements. (max_suggested_slice_pattern_length: u64 = 3), /// Lint: AWAIT_HOLDING_INVALID_TYPE. @@ -459,6 +459,10 @@ define_Conf! { /// Whether to **only** check for missing documentation in items visible within the current /// crate. For example, `pub(crate)` items. (missing_docs_in_crate_items: bool = false), + /// Lint: LARGE_FUTURES. + /// + /// The maximum byte size a `Future` can have, before it triggers the `clippy::large_futures` lint + (future_size_threshold: u64 = 16 * 1024), } /// Search for the configuration file. @@ -466,7 +470,7 @@ define_Conf! { /// # Errors /// /// Returns any unexpected filesystem error encountered when searching for the config file -pub fn lookup_conf_file() -> io::Result<Option<PathBuf>> { +pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> { /// Possible filename to search for. const CONFIG_FILE_NAMES: [&str; 2] = [".clippy.toml", "clippy.toml"]; @@ -474,9 +478,11 @@ pub fn lookup_conf_file() -> io::Result<Option<PathBuf>> { // If neither of those exist, use ".". let mut current = env::var_os("CLIPPY_CONF_DIR") .or_else(|| env::var_os("CARGO_MANIFEST_DIR")) - .map_or_else(|| PathBuf::from("."), PathBuf::from); + .map_or_else(|| PathBuf::from("."), PathBuf::from) + .canonicalize()?; let mut found_config: Option<PathBuf> = None; + let mut warnings = vec![]; loop { for config_file_name in &CONFIG_FILE_NAMES { @@ -487,12 +493,12 @@ pub fn lookup_conf_file() -> io::Result<Option<PathBuf>> { Ok(md) if md.is_dir() => {}, Ok(_) => { // warn if we happen to find two config files #8323 - if let Some(ref found_config_) = found_config { - eprintln!( - "Using config file `{}`\nWarning: `{}` will be ignored.", - found_config_.display(), - config_file.display(), - ); + if let Some(ref found_config) = found_config { + warnings.push(format!( + "using config file `{}`, `{}` will be ignored", + found_config.display(), + config_file.display() + )); } else { found_config = Some(config_file); } @@ -502,12 +508,12 @@ pub fn lookup_conf_file() -> io::Result<Option<PathBuf>> { } if found_config.is_some() { - return Ok(found_config); + return Ok((found_config, warnings)); } // If the current directory has no parent, we're done searching. if !current.pop() { - return Ok(None); + return Ok((None, warnings)); } } } diff --git a/src/tools/clippy/clippy_lints/src/utils/format_args_collector.rs b/src/tools/clippy/clippy_lints/src/utils/format_args_collector.rs index be56b842b98..09fcb82c37c 100644 --- a/src/tools/clippy/clippy_lints/src/utils/format_args_collector.rs +++ b/src/tools/clippy/clippy_lints/src/utils/format_args_collector.rs @@ -1,7 +1,12 @@ use clippy_utils::macros::collect_ast_format_args; -use rustc_ast::{Expr, ExprKind}; +use clippy_utils::source::snippet_opt; +use itertools::Itertools; +use rustc_ast::{Expr, ExprKind, FormatArgs}; +use rustc_lexer::{tokenize, TokenKind}; use rustc_lint::{EarlyContext, EarlyLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::hygiene; +use std::iter::once; declare_clippy_lint! { /// ### What it does @@ -15,9 +20,79 @@ declare_clippy_lint! { declare_lint_pass!(FormatArgsCollector => [FORMAT_ARGS_COLLECTOR]); impl EarlyLintPass for FormatArgsCollector { - fn check_expr(&mut self, _: &EarlyContext<'_>, expr: &Expr) { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { if let ExprKind::FormatArgs(args) = &expr.kind { + if has_span_from_proc_macro(cx, args) { + return; + } + collect_ast_format_args(expr.span, args); } } } + +/// Detects if the format string or an argument has its span set by a proc macro to something inside +/// a macro callsite, e.g. +/// +/// ```ignore +/// println!(some_proc_macro!("input {}"), a); +/// ``` +/// +/// Where `some_proc_macro` expands to +/// +/// ```ignore +/// println!("output {}", a); +/// ``` +/// +/// But with the span of `"output {}"` set to the macro input +/// +/// ```ignore +/// println!(some_proc_macro!("input {}"), a); +/// // ^^^^^^^^^^ +/// ``` +fn has_span_from_proc_macro(cx: &EarlyContext<'_>, args: &FormatArgs) -> bool { + let ctxt = args.span.ctxt(); + + // `format!("{} {} {c}", "one", "two", c = "three")` + // ^^^^^ ^^^^^ ^^^^^^^ + let argument_span = args + .arguments + .explicit_args() + .iter() + .map(|argument| hygiene::walk_chain(argument.expr.span, ctxt)); + + // `format!("{} {} {c}", "one", "two", c = "three")` + // ^^ ^^ ^^^^^^ + let between_spans = once(args.span) + .chain(argument_span) + .tuple_windows() + .map(|(start, end)| start.between(end)); + + for between_span in between_spans { + let mut seen_comma = false; + + let Some(snippet) = snippet_opt(cx, between_span) else { return true }; + for token in tokenize(&snippet) { + match token.kind { + TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } | TokenKind::Whitespace => {}, + TokenKind::Comma if !seen_comma => seen_comma = true, + // named arguments, `start_val, name = end_val` + // ^^^^^^^^^ between_span + TokenKind::Ident | TokenKind::Eq if seen_comma => {}, + // An unexpected token usually indicates that we crossed a macro boundary + // + // `println!(some_proc_macro!("input {}"), a)` + // ^^^ between_span + // `println!("{}", val!(x))` + // ^^^^^^^ between_span + _ => return true, + } + } + + if !seen_comma { + return true; + } + } + + false +} diff --git a/src/tools/clippy/clippy_lints/src/write.rs b/src/tools/clippy/clippy_lints/src/write.rs index 8114a8463fa..d7c94b909bd 100644 --- a/src/tools/clippy/clippy_lints/src/write.rs +++ b/src/tools/clippy/clippy_lints/src/write.rs @@ -463,12 +463,18 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) { && let Some(value_string) = snippet_opt(cx, arg.expr.span) { let (replacement, replace_raw) = match lit.kind { - LitKind::Str | LitKind::StrRaw(_) => extract_str_literal(&value_string), + LitKind::Str | LitKind::StrRaw(_) => match extract_str_literal(&value_string) { + Some(extracted) => extracted, + None => return, + }, LitKind::Char => ( match lit.symbol.as_str() { "\"" => "\\\"", "\\'" => "'", - _ => &value_string[1..value_string.len() - 1], + _ => match value_string.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) { + Some(stripped) => stripped, + None => return, + }, } .to_string(), false, @@ -533,13 +539,13 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) { /// `r#"a"#` -> (`a`, true) /// /// `"b"` -> (`b`, false) -fn extract_str_literal(literal: &str) -> (String, bool) { +fn extract_str_literal(literal: &str) -> Option<(String, bool)> { let (literal, raw) = match literal.strip_prefix('r') { Some(stripped) => (stripped.trim_matches('#'), true), None => (literal, false), }; - (literal[1..literal.len() - 1].to_string(), raw) + Some((literal.strip_prefix('"')?.strip_suffix('"')?.to_string(), raw)) } enum UnescapeErr { |
