Rollup merge of #148340 - flip1995:clippy-subtree-update, r=Manishearth

Clippy subtree update

r? ``@Manishearth``

Cargo.lock update due to Clippy version bump
This commit is contained in:
Matthias Krüger 2025-11-01 08:25:48 +01:00 committed by GitHub
commit 1ca21edce4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
235 changed files with 4525 additions and 2790 deletions

View file

@ -580,7 +580,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "clippy"
version = "0.1.92"
version = "0.1.93"
dependencies = [
"anstream",
"askama",
@ -607,7 +607,7 @@ dependencies = [
[[package]]
name = "clippy_config"
version = "0.1.92"
version = "0.1.93"
dependencies = [
"clippy_utils",
"itertools",
@ -630,7 +630,7 @@ dependencies = [
[[package]]
name = "clippy_lints"
version = "0.1.92"
version = "0.1.93"
dependencies = [
"arrayvec",
"cargo_metadata 0.18.1",
@ -662,7 +662,7 @@ dependencies = [
[[package]]
name = "clippy_utils"
version = "0.1.92"
version = "0.1.93"
dependencies = [
"arrayvec",
"itertools",
@ -1066,7 +1066,7 @@ dependencies = [
[[package]]
name = "declare_clippy_lint"
version = "0.1.92"
version = "0.1.93"
[[package]]
name = "derive-where"

View file

@ -8,6 +8,89 @@ document.
[e9b7045...master](https://github.com/rust-lang/rust-clippy/compare/e9b7045...master)
## Rust 1.91
Current stable, released 2025-10-30
[View all 146 merged pull requests](https://github.com/rust-lang/rust-clippy/pulls?q=merged%3A2025-07-25T21%3A05%3A11Z..2025-09-04T22%3A34%3A27Z+base%3Amaster)
### New Lints
* Added [`possible_missing_else`] to `suspicious`
[#15317](https://github.com/rust-lang/rust-clippy/pull/15317)
### Moves and Deprecations
* Moved [`cognitive_complexity`] from `nursery` to `restriction`
[#15415](https://github.com/rust-lang/rust-clippy/pull/15415)
* Moved [`declare_interior_mutable_const`] from `style` to `suspicious`
[#15454](https://github.com/rust-lang/rust-clippy/pull/15454)
* Moved [`crosspointer_transmute`] from `complexity` to `suspicious`
[#15403](https://github.com/rust-lang/rust-clippy/pull/15403)
### Enhancements
* [`excessive_precision`] added `const_literal_digits_threshold` option to suppress overly precise constants.
[#15193](https://github.com/rust-lang/rust-clippy/pull/15193)
* [`unwrap_in_result`] rewritten for better accuracy; now lints on `.unwrap()` and `.expect()`
directly and no longer mixes `Result` and `Option`.
[#15445](https://github.com/rust-lang/rust-clippy/pull/15445)
* [`panic`] now works in `const` contexts.
[#15565](https://github.com/rust-lang/rust-clippy/pull/15565)
* [`implicit_clone`] now also lints `to_string` calls (merging [`string_to_string`] behavior).
[#14177](https://github.com/rust-lang/rust-clippy/pull/14177)
* [`collapsible_match`] improved suggestions to handle necessary ref/dereferencing.
[#14221](https://github.com/rust-lang/rust-clippy/pull/14221)
* [`map_identity`] now suggests making variables mutable when required; recognizes tuple struct restructuring.
[#15261](https://github.com/rust-lang/rust-clippy/pull/15261)
* [`option_map_unit_fn`] preserves `unsafe` blocks in suggestions.
[#15570](https://github.com/rust-lang/rust-clippy/pull/15570)
* [`unnecessary_mut_passed`] provides structured, clearer fix suggestions.
[#15438](https://github.com/rust-lang/rust-clippy/pull/15438)
* [`float_equality_without_abs`] now checks `f16` and `f128` types.
[#15054](https://github.com/rust-lang/rust-clippy/pull/15054)
* [`doc_markdown`] expanded whitelist (`InfiniBand`, `RoCE`, `PowerPC`) and improved handling of
identifiers like NixOS.
[#15558](https://github.com/rust-lang/rust-clippy/pull/15558)
* [`clone_on_ref_ptr`] now suggests fully qualified paths to avoid resolution errors.
[#15561](https://github.com/rust-lang/rust-clippy/pull/15561)
* [`manual_assert`] simplifies boolean expressions in suggested fixes.
[#15368](https://github.com/rust-lang/rust-clippy/pull/15368)
* [`four_forward_slashes`] warns about bare CR in comments and avoids invalid autofixes.
[#15175](https://github.com/rust-lang/rust-clippy/pull/15175)
### False Positive Fixes
* [`alloc_instead_of_core`] fixed FP when `alloc` is an alias
[#15581](https://github.com/rust-lang/rust-clippy/pull/15581)
* [`needless_range_loop`] fixed FP and FN when meeting multidimensional array
[#15486](https://github.com/rust-lang/rust-clippy/pull/15486)
* [`semicolon_inside_block`] fixed FP when attribute over expr is not enabled
[#15476](https://github.com/rust-lang/rust-clippy/pull/15476)
* [`unnested_or_patterns`] fixed FP on structs with only shorthand field patterns
[#15343](https://github.com/rust-lang/rust-clippy/pull/15343)
* [`match_ref_pats`] fixed FP on match scrutinee of never type
[#15474](https://github.com/rust-lang/rust-clippy/pull/15474)
* [`infinite_loop`] fixed FP in async blocks that are not awaited
[#15157](https://github.com/rust-lang/rust-clippy/pull/15157)
* [`iter_on_single_items`] fixed FP on function pointers and let statements
[#15013](https://github.com/rust-lang/rust-clippy/pull/15013)
### ICE Fixes
* [`len_zero`] fix ICE when fn len has a return type without generic type params
[#15660](https://github.com/rust-lang/rust-clippy/pull/15660)
### Documentation Improvements
* [`cognitive_complexity`] corrected documentation to state lint is in `restriction`, not `nursery`
[#15563](https://github.com/rust-lang/rust-clippy/pull/15563)
### Performance Improvements
* [`doc_broken_link`] optimized by 99.77% (12M → 27k instructions)
[#15385](https://github.com/rust-lang/rust-clippy/pull/15385)
## Rust 1.90
Current stable, released 2025-09-18
@ -6253,6 +6336,7 @@ Released 2018-09-13
[`empty_drop`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_drop
[`empty_enum`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_enum
[`empty_enum_variants_with_brackets`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_enum_variants_with_brackets
[`empty_enums`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_enums
[`empty_line_after_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_line_after_doc_comments
[`empty_line_after_outer_attr`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_line_after_outer_attr
[`empty_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_loop
@ -6583,6 +6667,7 @@ Released 2018-09-13
[`needless_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_else
[`needless_for_each`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_for_each
[`needless_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_if
[`needless_ifs`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_ifs
[`needless_late_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
[`needless_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
[`needless_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_match

View file

@ -1,3 +1,3 @@
# The Rust Code of Conduct
The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html).
The Code of Conduct for this repository [can be found online](https://rust-lang.org/policies/code-of-conduct/).

View file

@ -1,6 +1,6 @@
[package]
name = "clippy"
version = "0.1.92"
version = "0.1.93"
description = "A bunch of helpful lints to avoid common pitfalls in Rust"
repository = "https://github.com/rust-lang/rust-clippy"
readme = "README.md"

View file

@ -1,8 +1,6 @@
[book]
authors = ["The Rust Clippy Developers"]
language = "en"
multilingual = false
src = "src"
title = "Clippy Documentation"
[rust]

View file

@ -21,14 +21,13 @@ use clippy_utils::sym;
impl<'tcx> LateLintPass<'tcx> for OurFancyMethodLint {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
// Check our expr is calling a method with pattern matching
if let hir::ExprKind::MethodCall(path, _, [self_arg, ..], _) = &expr.kind
if let hir::ExprKind::MethodCall(path, _, _, _) = &expr.kind
// Check if the name of this method is `our_fancy_method`
&& path.ident.name == sym::our_fancy_method
// We can check the type of the self argument whenever necessary.
// (It's necessary if we want to check that method is specifically belonging to a specific trait,
// for example, a `map` method could belong to user-defined trait instead of to `Iterator`)
// Check if the method belongs to the `sym::OurFancyTrait` trait.
// (for example, a `map` method could belong to user-defined trait instead of to `Iterator`)
// See the next section for more information.
&& cx.ty_based_def(self_arg).opt_parent(cx).is_diag_item(cx, sym::OurFancyTrait)
&& cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::OurFancyTrait)
{
println!("`expr` is a method call for `our_fancy_method`");
}
@ -45,6 +44,12 @@ New symbols such as `our_fancy_method` need to be added to the `clippy_utils::sy
This module extends the list of symbols already provided by the compiler crates
in `rustc_span::sym`.
If a trait defines only one method (such as the `std::ops::Deref` trait, which only has the `deref()` method),
one might be tempted to omit the method name check. This would work, but is not always advisable because:
- If a new method (possibly with a default implementation) were to be added to the trait, there would be a risk of
matching the wrong method.
- Comparing symbols is very cheap and might prevent a more expensive lookup.
## Checking if a `impl` block implements a method
While sometimes we want to check whether a method is being called or not, other

View file

@ -859,6 +859,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
* [`io_other_error`](https://rust-lang.github.io/rust-clippy/master/index.html#io_other_error)
* [`iter_kv_map`](https://rust-lang.github.io/rust-clippy/master/index.html#iter_kv_map)
* [`legacy_numeric_constants`](https://rust-lang.github.io/rust-clippy/master/index.html#legacy_numeric_constants)
* [`len_zero`](https://rust-lang.github.io/rust-clippy/master/index.html#len_zero)
* [`lines_filter_map_ok`](https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok)
* [`manual_abs_diff`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_abs_diff)
* [`manual_bits`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits)

View file

@ -1,6 +1,6 @@
[package]
name = "clippy_config"
version = "0.1.92"
version = "0.1.93"
edition = "2024"
publish = false

View file

@ -748,6 +748,7 @@ define_Conf! {
io_other_error,
iter_kv_map,
legacy_numeric_constants,
len_zero,
lines_filter_map_ok,
manual_abs_diff,
manual_bits,

View file

@ -1,4 +1,5 @@
use crate::update_lints::{DeprecatedLint, Lint, find_lint_decls, generate_lint_files, read_deprecated_lints};
use crate::parse::{DeprecatedLint, Lint, ParseCx};
use crate::update_lints::generate_lint_files;
use crate::utils::{UpdateMode, Version};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
@ -13,21 +14,20 @@ use std::{fs, io};
/// # Panics
///
/// If a file path could not read from or written to
pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
if let Some((prefix, _)) = name.split_once("::") {
panic!("`{name}` should not contain the `{prefix}` prefix");
}
let mut lints = find_lint_decls();
let (mut deprecated_lints, renamed_lints) = read_deprecated_lints();
pub fn deprecate<'cx>(cx: ParseCx<'cx>, clippy_version: Version, name: &'cx str, reason: &'cx str) {
let mut lints = cx.find_lint_decls();
let (mut deprecated_lints, renamed_lints) = cx.read_deprecated_lints();
let Some(lint) = lints.iter().find(|l| l.name == name) else {
eprintln!("error: failed to find lint `{name}`");
return;
};
let prefixed_name = String::from_iter(["clippy::", name]);
match deprecated_lints.binary_search_by(|x| x.name.cmp(&prefixed_name)) {
let prefixed_name = cx.str_buf.with(|buf| {
buf.extend(["clippy::", name]);
cx.arena.alloc_str(buf)
});
match deprecated_lints.binary_search_by(|x| x.name.cmp(prefixed_name)) {
Ok(_) => {
println!("`{name}` is already deprecated");
return;
@ -36,8 +36,8 @@ pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
idx,
DeprecatedLint {
name: prefixed_name,
reason: reason.into(),
version: clippy_version.rust_display().to_string(),
reason,
version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()),
},
),
}
@ -61,8 +61,8 @@ pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
}
}
fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec<Lint>) -> io::Result<bool> {
fn remove_lint(name: &str, lints: &mut Vec<Lint>) {
fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec<Lint<'_>>) -> io::Result<bool> {
fn remove_lint(name: &str, lints: &mut Vec<Lint<'_>>) {
lints.iter().position(|l| l.name == name).map(|pos| lints.remove(pos));
}
@ -135,14 +135,14 @@ fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec<Lint>) -> io
);
assert!(
content[lint.declaration_range.clone()].contains(&name.to_uppercase()),
content[lint.declaration_range].contains(&name.to_uppercase()),
"error: `{}` does not contain lint `{}`'s declaration",
path.display(),
lint.name
);
// Remove lint declaration (declare_clippy_lint!)
content.replace_range(lint.declaration_range.clone(), "");
content.replace_range(lint.declaration_range, "");
// Remove the module declaration (mod xyz;)
let mod_decl = format!("\nmod {name};");

View file

@ -268,7 +268,7 @@ fn run_rustfmt(update_mode: UpdateMode) {
.expect("invalid rustfmt path");
rustfmt_path.truncate(rustfmt_path.trim_end().len());
let args: Vec<_> = walk_dir_no_dot_or_target()
let args: Vec<_> = walk_dir_no_dot_or_target(".")
.filter_map(|e| {
let e = expect_action(e, ErrAction::Read, ".");
e.path()

View file

@ -1,9 +1,12 @@
#![feature(
rustc_private,
exit_status_error,
if_let_guard,
new_range,
new_range_api,
os_str_slice,
os_string_truncate,
pattern,
rustc_private,
slice_split_once
)]
#![warn(
@ -15,6 +18,7 @@
)]
#![allow(clippy::missing_panics_doc)]
extern crate rustc_arena;
#[expect(unused_extern_crates, reason = "required to link to rustc crates")]
extern crate rustc_driver;
extern crate rustc_lexer;
@ -32,5 +36,8 @@ pub mod setup;
pub mod sync;
pub mod update_lints;
mod parse;
mod utils;
pub use utils::{ClippyInfo, UpdateMode};
pub use self::parse::{ParseCx, new_parse_cx};
pub use self::utils::{ClippyInfo, UpdateMode};

View file

@ -4,10 +4,9 @@
use clap::{Args, Parser, Subcommand};
use clippy_dev::{
ClippyInfo, UpdateMode, deprecate_lint, dogfood, fmt, lint, new_lint, release, rename_lint, serve, setup, sync,
update_lints,
ClippyInfo, UpdateMode, deprecate_lint, dogfood, fmt, lint, new_lint, new_parse_cx, release, rename_lint, serve,
setup, sync, update_lints,
};
use std::convert::Infallible;
use std::env;
fn main() {
@ -28,7 +27,7 @@ fn main() {
allow_no_vcs,
} => dogfood::dogfood(fix, allow_dirty, allow_staged, allow_no_vcs),
DevCommand::Fmt { check } => fmt::run(UpdateMode::from_check(check)),
DevCommand::UpdateLints { check } => update_lints::update(UpdateMode::from_check(check)),
DevCommand::UpdateLints { check } => new_parse_cx(|cx| update_lints::update(cx, UpdateMode::from_check(check))),
DevCommand::NewLint {
pass,
name,
@ -36,7 +35,7 @@ fn main() {
r#type,
msrv,
} => match new_lint::create(clippy.version, pass, &name, &category, r#type.as_deref(), msrv) {
Ok(()) => update_lints::update(UpdateMode::Change),
Ok(()) => new_parse_cx(|cx| update_lints::update(cx, UpdateMode::Change)),
Err(e) => eprintln!("Unable to create lint: {e}"),
},
DevCommand::Setup(SetupCommand { subcommand }) => match subcommand {
@ -79,13 +78,18 @@ fn main() {
old_name,
new_name,
uplift,
} => rename_lint::rename(
clippy.version,
&old_name,
new_name.as_ref().unwrap_or(&old_name),
uplift,
),
DevCommand::Deprecate { name, reason } => deprecate_lint::deprecate(clippy.version, &name, &reason),
} => new_parse_cx(|cx| {
rename_lint::rename(
cx,
clippy.version,
&old_name,
new_name.as_ref().unwrap_or(&old_name),
uplift,
);
}),
DevCommand::Deprecate { name, reason } => {
new_parse_cx(|cx| deprecate_lint::deprecate(cx, clippy.version, &name, &reason));
},
DevCommand::Sync(SyncCommand { subcommand }) => match subcommand {
SyncSubcommand::UpdateNightly => sync::update_nightly(),
},
@ -95,6 +99,20 @@ fn main() {
}
}
fn lint_name(name: &str) -> Result<String, String> {
let name = name.replace('-', "_");
if let Some((pre, _)) = name.split_once("::") {
Err(format!("lint name should not contain the `{pre}` prefix"))
} else if name
.bytes()
.any(|x| !matches!(x, b'_' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z'))
{
Err("lint name contains invalid characters".to_owned())
} else {
Ok(name)
}
}
#[derive(Parser)]
#[command(name = "dev", about)]
struct Dev {
@ -150,7 +168,7 @@ enum DevCommand {
#[arg(
short,
long,
value_parser = |name: &str| Ok::<_, Infallible>(name.replace('-', "_")),
value_parser = lint_name,
)]
/// Name of the new lint in snake case, ex: `fn_too_long`
name: String,
@ -223,8 +241,12 @@ enum DevCommand {
/// Rename a lint
RenameLint {
/// The name of the lint to rename
#[arg(value_parser = lint_name)]
old_name: String,
#[arg(required_unless_present = "uplift")]
#[arg(
required_unless_present = "uplift",
value_parser = lint_name,
)]
/// The new name of the lint
new_name: Option<String>,
#[arg(long)]
@ -234,6 +256,7 @@ enum DevCommand {
/// Deprecate the given lint
Deprecate {
/// The name of the lint to deprecate
#[arg(value_parser = lint_name)]
name: String,
#[arg(long, short)]
/// The reason for deprecation

View file

@ -1,4 +1,5 @@
use crate::utils::{RustSearcher, Token, Version};
use crate::parse::cursor::{self, Capture, Cursor};
use crate::utils::Version;
use clap::ValueEnum;
use indoc::{formatdoc, writedoc};
use std::fmt::{self, Write as _};
@ -516,22 +517,22 @@ fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str>
// Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) {
#[allow(clippy::enum_glob_use)]
use Token::*;
use cursor::Pat::*;
let mut context = None;
let mut decl_end = None;
let mut searcher = RustSearcher::new(contents);
while let Some(name) = searcher.find_capture_token(CaptureIdent) {
match name {
let mut cursor = Cursor::new(contents);
let mut captures = [Capture::EMPTY];
while let Some(name) = cursor.find_any_ident() {
match cursor.get_text(name) {
"declare_clippy_lint" => {
if searcher.match_tokens(&[Bang, OpenBrace], &mut []) && searcher.find_token(CloseBrace) {
decl_end = Some(searcher.pos());
if cursor.match_all(&[Bang, OpenBrace], &mut []) && cursor.find_pat(CloseBrace) {
decl_end = Some(cursor.pos());
}
},
"impl" => {
let mut capture = "";
if searcher.match_tokens(&[Lt, Lifetime, Gt, CaptureIdent], &mut [&mut capture]) {
match capture {
if cursor.match_all(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) {
match cursor.get_text(captures[0]) {
"LateLintPass" => context = Some("LateContext"),
"EarlyLintPass" => context = Some("EarlyContext"),
_ => {},

View file

@ -0,0 +1,285 @@
pub mod cursor;
use self::cursor::{Capture, Cursor};
use crate::utils::{ErrAction, File, Scoped, expect_action, walk_dir_no_dot_or_target};
use core::fmt::{Display, Write as _};
use core::range::Range;
use rustc_arena::DroplessArena;
use std::fs;
use std::path::{self, Path, PathBuf};
use std::str::pattern::Pattern;
pub struct ParseCxImpl<'cx> {
pub arena: &'cx DroplessArena,
pub str_buf: StrBuf,
}
pub type ParseCx<'cx> = &'cx mut ParseCxImpl<'cx>;
/// Calls the given function inside a newly created parsing context.
pub fn new_parse_cx<'env, T>(f: impl for<'cx> FnOnce(&'cx mut Scoped<'cx, 'env, ParseCxImpl<'cx>>) -> T) -> T {
let arena = DroplessArena::default();
f(&mut Scoped::new(ParseCxImpl {
arena: &arena,
str_buf: StrBuf::with_capacity(128),
}))
}
/// A string used as a temporary buffer used to avoid allocating for short lived strings.
pub struct StrBuf(String);
impl StrBuf {
/// Creates a new buffer with the specified initial capacity.
pub fn with_capacity(cap: usize) -> Self {
Self(String::with_capacity(cap))
}
/// Allocates the result of formatting the given value onto the arena.
pub fn alloc_display<'cx>(&mut self, arena: &'cx DroplessArena, value: impl Display) -> &'cx str {
self.0.clear();
write!(self.0, "{value}").expect("`Display` impl returned an error");
arena.alloc_str(&self.0)
}
/// Allocates the string onto the arena with all ascii characters converted to
/// lowercase.
pub fn alloc_ascii_lower<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str {
self.0.clear();
self.0.push_str(s);
self.0.make_ascii_lowercase();
arena.alloc_str(&self.0)
}
/// Allocates the result of replacing all instances the pattern with the given string
/// onto the arena.
pub fn alloc_replaced<'cx>(
&mut self,
arena: &'cx DroplessArena,
s: &str,
pat: impl Pattern,
replacement: &str,
) -> &'cx str {
let mut parts = s.split(pat);
let Some(first) = parts.next() else {
return "";
};
self.0.clear();
self.0.push_str(first);
for part in parts {
self.0.push_str(replacement);
self.0.push_str(part);
}
if self.0.is_empty() {
""
} else {
arena.alloc_str(&self.0)
}
}
/// Performs an operation with the freshly cleared buffer.
pub fn with<T>(&mut self, f: impl FnOnce(&mut String) -> T) -> T {
self.0.clear();
f(&mut self.0)
}
}
pub struct Lint<'cx> {
pub name: &'cx str,
pub group: &'cx str,
pub module: &'cx str,
pub path: PathBuf,
pub declaration_range: Range<usize>,
}
pub struct DeprecatedLint<'cx> {
pub name: &'cx str,
pub reason: &'cx str,
pub version: &'cx str,
}
pub struct RenamedLint<'cx> {
pub old_name: &'cx str,
pub new_name: &'cx str,
pub version: &'cx str,
}
impl<'cx> ParseCxImpl<'cx> {
/// Finds all lint declarations (`declare_clippy_lint!`)
#[must_use]
pub fn find_lint_decls(&mut self) -> Vec<Lint<'cx>> {
let mut lints = Vec::with_capacity(1000);
let mut contents = String::new();
for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") {
let e = expect_action(e, ErrAction::Read, ".");
// Skip if this isn't a lint crate's directory.
let mut crate_path = if expect_action(e.file_type(), ErrAction::Read, ".").is_dir()
&& let Ok(crate_path) = e.file_name().into_string()
&& crate_path.starts_with("clippy_lints")
&& crate_path != "clippy_lints_internal"
{
crate_path
} else {
continue;
};
crate_path.push(path::MAIN_SEPARATOR);
crate_path.push_str("src");
for e in walk_dir_no_dot_or_target(&crate_path) {
let e = expect_action(e, ErrAction::Read, &crate_path);
if let Some(path) = e.path().to_str()
&& let Some(path) = path.strip_suffix(".rs")
&& let Some(path) = path.get(crate_path.len() + 1..)
{
let module = if path == "lib" {
""
} else {
let path = path
.strip_suffix("mod")
.and_then(|x| x.strip_suffix(path::MAIN_SEPARATOR))
.unwrap_or(path);
self.str_buf
.alloc_replaced(self.arena, path, path::MAIN_SEPARATOR, "::")
};
self.parse_clippy_lint_decls(
e.path(),
File::open_read_to_cleared_string(e.path(), &mut contents),
module,
&mut lints,
);
}
}
}
lints.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
lints
}
/// Parse a source file looking for `declare_clippy_lint` macro invocations.
fn parse_clippy_lint_decls(&mut self, path: &Path, contents: &str, module: &'cx str, lints: &mut Vec<Lint<'cx>>) {
#[allow(clippy::enum_glob_use)]
use cursor::Pat::*;
#[rustfmt::skip]
static DECL_TOKENS: &[cursor::Pat<'_>] = &[
// !{ /// docs
Bang, OpenBrace, AnyComment,
// #[clippy::version = "version"]
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
// pub NAME, GROUP,
Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma,
];
let mut cursor = Cursor::new(contents);
let mut captures = [Capture::EMPTY; 2];
while let Some(start) = cursor.find_ident("declare_clippy_lint") {
if cursor.match_all(DECL_TOKENS, &mut captures) && cursor.find_pat(CloseBrace) {
lints.push(Lint {
name: self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[0])),
group: self.arena.alloc_str(cursor.get_text(captures[1])),
module,
path: path.into(),
declaration_range: start as usize..cursor.pos() as usize,
});
}
}
}
#[must_use]
pub fn read_deprecated_lints(&mut self) -> (Vec<DeprecatedLint<'cx>>, Vec<RenamedLint<'cx>>) {
#[allow(clippy::enum_glob_use)]
use cursor::Pat::*;
#[rustfmt::skip]
static DECL_TOKENS: &[cursor::Pat<'_>] = &[
// #[clippy::version = "version"]
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket,
// ("first", "second"),
OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma,
];
#[rustfmt::skip]
static DEPRECATED_TOKENS: &[cursor::Pat<'_>] = &[
// !{ DEPRECATED(DEPRECATED_VERSION) = [
Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket,
];
#[rustfmt::skip]
static RENAMED_TOKENS: &[cursor::Pat<'_>] = &[
// !{ RENAMED(RENAMED_VERSION) = [
Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket,
];
let path = "clippy_lints/src/deprecated_lints.rs";
let mut deprecated = Vec::with_capacity(30);
let mut renamed = Vec::with_capacity(80);
let mut contents = String::new();
File::open_read_to_cleared_string(path, &mut contents);
let mut cursor = Cursor::new(&contents);
let mut captures = [Capture::EMPTY; 3];
// First instance is the macro definition.
assert!(
cursor.find_ident("declare_with_version").is_some(),
"error reading deprecated lints"
);
if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(DEPRECATED_TOKENS, &mut []) {
while cursor.match_all(DECL_TOKENS, &mut captures) {
deprecated.push(DeprecatedLint {
name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[1])),
reason: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[2])),
version: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[0])),
});
}
} else {
panic!("error reading deprecated lints");
}
if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(RENAMED_TOKENS, &mut []) {
while cursor.match_all(DECL_TOKENS, &mut captures) {
renamed.push(RenamedLint {
old_name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[1])),
new_name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[2])),
version: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[0])),
});
}
} else {
panic!("error reading renamed lints");
}
deprecated.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
renamed.sort_by(|lhs, rhs| lhs.old_name.cmp(rhs.old_name));
(deprecated, renamed)
}
/// Removes the line splices and surrounding quotes from a string literal
fn parse_str_lit(&mut self, s: &str) -> &'cx str {
let (s, is_raw) = if let Some(s) = s.strip_prefix("r") {
(s.trim_matches('#'), true)
} else {
(s, false)
};
let s = s
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
if is_raw {
if s.is_empty() { "" } else { self.arena.alloc_str(s) }
} else {
self.str_buf.with(|buf| {
rustc_literal_escaper::unescape_str(s, &mut |_, ch| {
if let Ok(ch) = ch {
buf.push(ch);
}
});
if buf.is_empty() { "" } else { self.arena.alloc_str(buf) }
})
}
}
fn parse_str_single_line(&mut self, path: &Path, s: &str) -> &'cx str {
let value = self.parse_str_lit(s);
assert!(
!value.contains('\n'),
"error parsing `{}`: `{s}` should be a single line string",
path.display(),
);
value
}
}

View file

@ -0,0 +1,263 @@
use core::slice;
use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind};
/// A token pattern used for searching and matching by the [`Cursor`].
///
/// In the event that a pattern is a multi-token sequence, earlier tokens will be consumed
/// even if the pattern ultimately isn't matched. e.g. With the sequence `:*` matching
/// `DoubleColon` will consume the first `:` and then fail to match, leaving the cursor at
/// the `*`.
#[derive(Clone, Copy)]
pub enum Pat<'a> {
/// Matches any number of comments and doc comments.
AnyComment,
Ident(&'a str),
CaptureIdent,
LitStr,
CaptureLitStr,
Bang,
CloseBrace,
CloseBracket,
CloseParen,
Comma,
DoubleColon,
Eq,
Lifetime,
Lt,
Gt,
OpenBrace,
OpenBracket,
OpenParen,
Pound,
Semi,
}
#[derive(Clone, Copy)]
pub struct Capture {
pub pos: u32,
pub len: u32,
}
impl Capture {
pub const EMPTY: Self = Self { pos: 0, len: 0 };
}
/// A unidirectional cursor over a token stream that is lexed on demand.
pub struct Cursor<'txt> {
next_token: Token,
pos: u32,
inner: lex::Cursor<'txt>,
text: &'txt str,
}
impl<'txt> Cursor<'txt> {
#[must_use]
pub fn new(text: &'txt str) -> Self {
let mut inner = lex::Cursor::new(text, lex::FrontmatterAllowed::Yes);
Self {
next_token: inner.advance_token(),
pos: 0,
inner,
text,
}
}
/// Gets the text of the captured token assuming it came from this cursor.
#[must_use]
pub fn get_text(&self, capture: Capture) -> &'txt str {
&self.text[capture.pos as usize..(capture.pos + capture.len) as usize]
}
/// Gets the text that makes up the next token in the stream, or the empty string if
/// stream is exhausted.
#[must_use]
pub fn peek_text(&self) -> &'txt str {
&self.text[self.pos as usize..(self.pos + self.next_token.len) as usize]
}
/// Gets the length of the next token in bytes, or zero if the stream is exhausted.
#[must_use]
pub fn peek_len(&self) -> u32 {
self.next_token.len
}
/// Gets the next token in the stream, or [`TokenKind::Eof`] if the stream is
/// exhausted.
#[must_use]
pub fn peek(&self) -> TokenKind {
self.next_token.kind
}
/// Gets the offset of the next token in the source string, or the string's length if
/// the stream is exhausted.
#[must_use]
pub fn pos(&self) -> u32 {
self.pos
}
/// Gets whether the cursor has exhausted its input.
#[must_use]
pub fn at_end(&self) -> bool {
self.next_token.kind == TokenKind::Eof
}
/// Advances the cursor to the next token. If the stream is exhausted this will set
/// the next token to [`TokenKind::Eof`].
pub fn step(&mut self) {
// `next_token.len` is zero for the eof marker.
self.pos += self.next_token.len;
self.next_token = self.inner.advance_token();
}
/// Consumes tokens until the given pattern is either fully matched of fails to match.
/// Returns whether the pattern was fully matched.
///
/// For each capture made by the pattern one item will be taken from the capture
/// sequence with the result placed inside.
fn match_impl(&mut self, pat: Pat<'_>, captures: &mut slice::IterMut<'_, Capture>) -> bool {
loop {
match (pat, self.next_token.kind) {
#[rustfmt::skip] // rustfmt bug: https://github.com/rust-lang/rustfmt/issues/6697
(_, TokenKind::Whitespace)
| (
Pat::AnyComment,
TokenKind::BlockComment { terminated: true, .. } | TokenKind::LineComment { .. },
) => self.step(),
(Pat::AnyComment, _) => return true,
(Pat::Bang, TokenKind::Bang)
| (Pat::CloseBrace, TokenKind::CloseBrace)
| (Pat::CloseBracket, TokenKind::CloseBracket)
| (Pat::CloseParen, TokenKind::CloseParen)
| (Pat::Comma, TokenKind::Comma)
| (Pat::Eq, TokenKind::Eq)
| (Pat::Lifetime, TokenKind::Lifetime { .. })
| (Pat::Lt, TokenKind::Lt)
| (Pat::Gt, TokenKind::Gt)
| (Pat::OpenBrace, TokenKind::OpenBrace)
| (Pat::OpenBracket, TokenKind::OpenBracket)
| (Pat::OpenParen, TokenKind::OpenParen)
| (Pat::Pound, TokenKind::Pound)
| (Pat::Semi, TokenKind::Semi)
| (
Pat::LitStr,
TokenKind::Literal {
kind: LiteralKind::Str { terminated: true } | LiteralKind::RawStr { .. },
..
},
) => {
self.step();
return true;
},
(Pat::Ident(x), TokenKind::Ident) if x == self.peek_text() => {
self.step();
return true;
},
(Pat::DoubleColon, TokenKind::Colon) => {
self.step();
if !self.at_end() && matches!(self.next_token.kind, TokenKind::Colon) {
self.step();
return true;
}
return false;
},
#[rustfmt::skip]
(
Pat::CaptureLitStr,
TokenKind::Literal {
kind:
LiteralKind::Str { terminated: true }
| LiteralKind::RawStr { n_hashes: Some(_) },
..
},
)
| (Pat::CaptureIdent, TokenKind::Ident) => {
*captures.next().unwrap() = Capture { pos: self.pos, len: self.next_token.len };
self.step();
return true;
},
_ => return false,
}
}
}
/// Consumes all tokens until the specified identifier is found and returns its
/// position. Returns `None` if the identifier could not be found.
///
/// The cursor will be positioned immediately after the identifier, or at the end if
/// it is not.
pub fn find_ident(&mut self, ident: &str) -> Option<u32> {
loop {
match self.next_token.kind {
TokenKind::Ident if self.peek_text() == ident => {
let pos = self.pos;
self.step();
return Some(pos);
},
TokenKind::Eof => return None,
_ => self.step(),
}
}
}
/// Consumes all tokens until the next identifier is found and captures it. Returns
/// `None` if no identifier could be found.
///
/// The cursor will be positioned immediately after the identifier, or at the end if
/// it is not.
pub fn find_any_ident(&mut self) -> Option<Capture> {
loop {
match self.next_token.kind {
TokenKind::Ident => {
let res = Capture {
pos: self.pos,
len: self.next_token.len,
};
self.step();
return Some(res);
},
TokenKind::Eof => return None,
_ => self.step(),
}
}
}
/// Continually attempt to match the pattern on subsequent tokens until a match is
/// found. Returns whether the pattern was successfully matched.
///
/// Not generally suitable for multi-token patterns or patterns that can match
/// nothing.
#[must_use]
pub fn find_pat(&mut self, pat: Pat<'_>) -> bool {
let mut capture = [].iter_mut();
while !self.match_impl(pat, &mut capture) {
self.step();
if self.at_end() {
return false;
}
}
true
}
/// Attempts to match a sequence of patterns at the current position. Returns whether
/// all patterns were successfully matched.
///
/// Captures will be written to the given slice in the order they're matched. If a
/// capture is matched, but there are no more capture slots this will panic. If the
/// match is completed without filling all the capture slots they will be left
/// unmodified.
///
/// If the match fails the cursor will be positioned at the first failing token.
#[must_use]
pub fn match_all(&mut self, pats: &[Pat<'_>], captures: &mut [Capture]) -> bool {
let mut captures = captures.iter_mut();
pats.iter().all(|&p| self.match_impl(p, &mut captures))
}
/// Attempts to match a single pattern at the current position. Returns whether the
/// pattern was successfully matched.
///
/// If the pattern attempts to capture anything this will panic. If the match fails
/// the cursor will be positioned at the first failing token.
#[must_use]
pub fn match_pat(&mut self, pat: Pat<'_>) -> bool {
self.match_impl(pat, &mut [].iter_mut())
}
}

View file

@ -23,7 +23,7 @@ pub fn bump_version(mut version: Version) {
dst.push_str(&src[..package.version_range.start]);
write!(dst, "\"{}\"", version.toml_display()).unwrap();
dst.push_str(&src[package.version_range.end..]);
UpdateStatus::from_changed(src.get(package.version_range.clone()) != dst.get(package.version_range))
UpdateStatus::from_changed(src.get(package.version_range) != dst.get(package.version_range))
}
});
}

View file

@ -1,7 +1,9 @@
use crate::update_lints::{RenamedLint, find_lint_decls, generate_lint_files, read_deprecated_lints};
use crate::parse::cursor::{self, Capture, Cursor};
use crate::parse::{ParseCx, RenamedLint};
use crate::update_lints::generate_lint_files;
use crate::utils::{
ErrAction, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, Version, delete_dir_if_exists,
delete_file_if_exists, expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target,
ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists,
expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target,
};
use rustc_lexer::TokenKind;
use std::ffi::OsString;
@ -24,36 +26,35 @@ use std::path::Path;
/// * If `old_name` doesn't name an existing lint.
/// * If `old_name` names a deprecated or renamed lint.
#[expect(clippy::too_many_lines)]
pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: bool) {
if let Some((prefix, _)) = old_name.split_once("::") {
panic!("`{old_name}` should not contain the `{prefix}` prefix");
}
if let Some((prefix, _)) = new_name.split_once("::") {
panic!("`{new_name}` should not contain the `{prefix}` prefix");
}
pub fn rename<'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_name: &'cx str, new_name: &'cx str, uplift: bool) {
let mut updater = FileUpdater::default();
let mut lints = find_lint_decls();
let (deprecated_lints, mut renamed_lints) = read_deprecated_lints();
let mut lints = cx.find_lint_decls();
let (deprecated_lints, mut renamed_lints) = cx.read_deprecated_lints();
let Ok(lint_idx) = lints.binary_search_by(|x| x.name.as_str().cmp(old_name)) else {
let Ok(lint_idx) = lints.binary_search_by(|x| x.name.cmp(old_name)) else {
panic!("could not find lint `{old_name}`");
};
let lint = &lints[lint_idx];
let old_name_prefixed = String::from_iter(["clippy::", old_name]);
let old_name_prefixed = cx.str_buf.with(|buf| {
buf.extend(["clippy::", old_name]);
cx.arena.alloc_str(buf)
});
let new_name_prefixed = if uplift {
new_name.to_owned()
new_name
} else {
String::from_iter(["clippy::", new_name])
cx.str_buf.with(|buf| {
buf.extend(["clippy::", new_name]);
cx.arena.alloc_str(buf)
})
};
for lint in &mut renamed_lints {
if lint.new_name == old_name_prefixed {
lint.new_name.clone_from(&new_name_prefixed);
lint.new_name = new_name_prefixed;
}
}
match renamed_lints.binary_search_by(|x| x.old_name.cmp(&old_name_prefixed)) {
match renamed_lints.binary_search_by(|x| x.old_name.cmp(old_name_prefixed)) {
Ok(_) => {
println!("`{old_name}` already has a rename registered");
return;
@ -63,12 +64,8 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
idx,
RenamedLint {
old_name: old_name_prefixed,
new_name: if uplift {
new_name.to_owned()
} else {
String::from_iter(["clippy::", new_name])
},
version: clippy_version.rust_display().to_string(),
new_name: new_name_prefixed,
version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()),
},
);
},
@ -104,7 +101,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
}
delete_test_files(old_name, change_prefixed_tests);
lints.remove(lint_idx);
} else if lints.binary_search_by(|x| x.name.as_str().cmp(new_name)).is_err() {
} else if lints.binary_search_by(|x| x.name.cmp(new_name)).is_err() {
let lint = &mut lints[lint_idx];
if lint.module.ends_with(old_name)
&& lint
@ -118,13 +115,15 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
mod_edit = ModEdit::Rename;
}
let mod_len = lint.module.len();
lint.module.truncate(mod_len - old_name.len());
lint.module.push_str(new_name);
lint.module = cx.str_buf.with(|buf| {
buf.push_str(&lint.module[..lint.module.len() - old_name.len()]);
buf.push_str(new_name);
cx.arena.alloc_str(buf)
});
}
rename_test_files(old_name, new_name, change_prefixed_tests);
new_name.clone_into(&mut lints[lint_idx].name);
lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
lints[lint_idx].name = new_name;
lints.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
} else {
println!("Renamed `clippy::{old_name}` to `clippy::{new_name}`");
println!("Since `{new_name}` already exists the existing code has not been changed");
@ -132,7 +131,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
}
let mut update_fn = file_update_fn(old_name, new_name, mod_edit);
for e in walk_dir_no_dot_or_target() {
for e in walk_dir_no_dot_or_target(".") {
let e = expect_action(e, ErrAction::Read, ".");
if e.path().as_os_str().as_encoded_bytes().ends_with(b".rs") {
updater.update_file(e.path(), &mut update_fn);
@ -285,47 +284,46 @@ fn file_update_fn<'a, 'b>(
move |_, src, dst| {
let mut copy_pos = 0u32;
let mut changed = false;
let mut searcher = RustSearcher::new(src);
let mut capture = "";
let mut cursor = Cursor::new(src);
let mut captures = [Capture::EMPTY];
loop {
match searcher.peek() {
match cursor.peek() {
TokenKind::Eof => break,
TokenKind::Ident => {
let match_start = searcher.pos();
let text = searcher.peek_text();
searcher.step();
let match_start = cursor.pos();
let text = cursor.peek_text();
cursor.step();
match text {
// clippy::line_name or clippy::lint-name
"clippy" => {
if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture])
&& capture == old_name
if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures)
&& cursor.get_text(captures[0]) == old_name
{
dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]);
dst.push_str(new_name);
copy_pos = searcher.pos();
copy_pos = cursor.pos();
changed = true;
}
},
// mod lint_name
"mod" => {
if !matches!(mod_edit, ModEdit::None)
&& searcher.match_tokens(&[Token::CaptureIdent], &mut [&mut capture])
&& capture == old_name
&& let Some(pos) = cursor.find_ident(old_name)
{
match mod_edit {
ModEdit::Rename => {
dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
dst.push_str(&src[copy_pos as usize..pos as usize]);
dst.push_str(new_name);
copy_pos = searcher.pos();
copy_pos = cursor.pos();
changed = true;
},
ModEdit::Delete if searcher.match_tokens(&[Token::Semi], &mut []) => {
ModEdit::Delete if cursor.match_pat(cursor::Pat::Semi) => {
let mut start = &src[copy_pos as usize..match_start as usize];
if start.ends_with("\n\n") {
start = &start[..start.len() - 1];
}
dst.push_str(start);
copy_pos = searcher.pos();
copy_pos = cursor.pos();
if src[copy_pos as usize..].starts_with("\n\n") {
copy_pos += 1;
}
@ -337,8 +335,8 @@ fn file_update_fn<'a, 'b>(
},
// lint_name::
name if matches!(mod_edit, ModEdit::Rename) && name == old_name => {
let name_end = searcher.pos();
if searcher.match_tokens(&[Token::DoubleColon], &mut []) {
let name_end = cursor.pos();
if cursor.match_pat(cursor::Pat::DoubleColon) {
dst.push_str(&src[copy_pos as usize..match_start as usize]);
dst.push_str(new_name);
copy_pos = name_end;
@ -356,36 +354,36 @@ fn file_update_fn<'a, 'b>(
};
dst.push_str(&src[copy_pos as usize..match_start as usize]);
dst.push_str(replacement);
copy_pos = searcher.pos();
copy_pos = cursor.pos();
changed = true;
},
}
},
// //~ lint_name
TokenKind::LineComment { doc_style: None } => {
let text = searcher.peek_text();
let text = cursor.peek_text();
if text.starts_with("//~")
&& let Some(text) = text.strip_suffix(old_name)
&& !text.ends_with(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_'))
{
dst.push_str(&src[copy_pos as usize..searcher.pos() as usize + text.len()]);
dst.push_str(&src[copy_pos as usize..cursor.pos() as usize + text.len()]);
dst.push_str(new_name);
copy_pos = searcher.pos() + searcher.peek_len();
copy_pos = cursor.pos() + cursor.peek_len();
changed = true;
}
searcher.step();
cursor.step();
},
// ::lint_name
TokenKind::Colon
if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture])
&& capture == old_name =>
if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures)
&& cursor.get_text(captures[0]) == old_name =>
{
dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]);
dst.push_str(new_name);
copy_pos = searcher.pos();
copy_pos = cursor.pos();
changed = true;
},
_ => searcher.step(),
_ => cursor.step(),
}
}

View file

@ -1,13 +1,10 @@
use crate::utils::{
ErrAction, File, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, expect_action, update_text_region_fn,
};
use crate::parse::cursor::Cursor;
use crate::parse::{DeprecatedLint, Lint, ParseCx, RenamedLint};
use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn};
use itertools::Itertools;
use std::collections::HashSet;
use std::fmt::Write;
use std::fs;
use std::ops::Range;
use std::path::{self, Path, PathBuf};
use walkdir::{DirEntry, WalkDir};
use std::path::{self, Path};
const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
// Use that command to update this file and do not edit by hand.\n\
@ -24,18 +21,18 @@ const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.ht
/// # Panics
///
/// Panics if a file path could not read from or then written to
pub fn update(update_mode: UpdateMode) {
let lints = find_lint_decls();
let (deprecated, renamed) = read_deprecated_lints();
pub fn update(cx: ParseCx<'_>, update_mode: UpdateMode) {
let lints = cx.find_lint_decls();
let (deprecated, renamed) = cx.read_deprecated_lints();
generate_lint_files(update_mode, &lints, &deprecated, &renamed);
}
#[expect(clippy::too_many_lines)]
pub fn generate_lint_files(
update_mode: UpdateMode,
lints: &[Lint],
deprecated: &[DeprecatedLint],
renamed: &[RenamedLint],
lints: &[Lint<'_>],
deprecated: &[DeprecatedLint<'_>],
renamed: &[RenamedLint<'_>],
) {
let mut updater = FileUpdater::default();
updater.update_file_checked(
@ -64,7 +61,7 @@ pub fn generate_lint_files(
|dst| {
for lint in lints
.iter()
.map(|l| &*l.name)
.map(|l| l.name)
.chain(deprecated.iter().filter_map(|l| l.name.strip_prefix("clippy::")))
.chain(renamed.iter().filter_map(|l| l.old_name.strip_prefix("clippy::")))
.sorted()
@ -79,13 +76,13 @@ pub fn generate_lint_files(
update_mode,
"clippy_lints/src/deprecated_lints.rs",
&mut |_, src, dst| {
let mut searcher = RustSearcher::new(src);
let mut cursor = Cursor::new(src);
assert!(
searcher.find_token(Token::Ident("declare_with_version"))
&& searcher.find_token(Token::Ident("declare_with_version")),
cursor.find_ident("declare_with_version").is_some()
&& cursor.find_ident("declare_with_version").is_some(),
"error reading deprecated lints"
);
dst.push_str(&src[..searcher.pos() as usize]);
dst.push_str(&src[..cursor.pos() as usize]);
dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n");
for lint in deprecated {
write!(
@ -135,13 +132,13 @@ pub fn generate_lint_files(
dst.push_str(GENERATED_FILE_COMMENT);
dst.push_str("#![allow(clippy::duplicated_attributes)]\n");
for lint in renamed {
if seen_lints.insert(&lint.new_name) {
if seen_lints.insert(lint.new_name) {
writeln!(dst, "#![allow({})]", lint.new_name).unwrap();
}
}
seen_lints.clear();
for lint in renamed {
if seen_lints.insert(&lint.old_name) {
if seen_lints.insert(lint.old_name) {
writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap();
}
}
@ -167,7 +164,7 @@ pub fn generate_lint_files(
for lint_mod in lints
.iter()
.filter(|l| !l.module.is_empty())
.map(|l| l.module.split_once("::").map_or(&*l.module, |x| x.0))
.map(|l| l.module.split_once("::").map_or(l.module, |x| x.0))
.sorted()
.dedup()
{
@ -200,260 +197,3 @@ pub fn generate_lint_files(
fn round_to_fifty(count: usize) -> usize {
count / 50 * 50
}
/// Lint data parsed from the Clippy source code.
#[derive(PartialEq, Eq, Debug)]
pub struct Lint {
pub name: String,
pub group: String,
pub module: String,
pub path: PathBuf,
pub declaration_range: Range<usize>,
}
pub struct DeprecatedLint {
pub name: String,
pub reason: String,
pub version: String,
}
pub struct RenamedLint {
pub old_name: String,
pub new_name: String,
pub version: String,
}
/// Finds all lint declarations (`declare_clippy_lint!`)
#[must_use]
pub fn find_lint_decls() -> Vec<Lint> {
let mut lints = Vec::with_capacity(1000);
let mut contents = String::new();
for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") {
let e = expect_action(e, ErrAction::Read, ".");
if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() {
continue;
}
let Ok(mut name) = e.file_name().into_string() else {
continue;
};
if name.starts_with("clippy_lints") && name != "clippy_lints_internal" {
name.push_str("/src");
for (file, module) in read_src_with_module(name.as_ref()) {
parse_clippy_lint_decls(
file.path(),
File::open_read_to_cleared_string(file.path(), &mut contents),
&module,
&mut lints,
);
}
}
}
lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
lints
}
/// Reads the source files from the given root directory
fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator<Item = (DirEntry, String)> {
WalkDir::new(src_root).into_iter().filter_map(move |e| {
let e = expect_action(e, ErrAction::Read, src_root);
let path = e.path().as_os_str().as_encoded_bytes();
if let Some(path) = path.strip_suffix(b".rs")
&& let Some(path) = path.get(src_root.as_os_str().len() + 1..)
{
if path == b"lib" {
Some((e, String::new()))
} else {
let path = if let Some(path) = path.strip_suffix(b"mod")
&& let Some(path) = path.strip_suffix(b"/").or_else(|| path.strip_suffix(b"\\"))
{
path
} else {
path
};
if let Ok(path) = str::from_utf8(path) {
let path = path.replace(['/', '\\'], "::");
Some((e, path))
} else {
None
}
}
} else {
None
}
})
}
/// Parse a source file looking for `declare_clippy_lint` macro invocations.
fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec<Lint>) {
#[allow(clippy::enum_glob_use)]
use Token::*;
#[rustfmt::skip]
static DECL_TOKENS: &[Token<'_>] = &[
// !{ /// docs
Bang, OpenBrace, AnyComment,
// #[clippy::version = "version"]
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
// pub NAME, GROUP,
Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma,
];
let mut searcher = RustSearcher::new(contents);
while searcher.find_token(Ident("declare_clippy_lint")) {
let start = searcher.pos() as usize - "declare_clippy_lint".len();
let (mut name, mut group) = ("", "");
if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) {
lints.push(Lint {
name: name.to_lowercase(),
group: group.into(),
module: module.into(),
path: path.into(),
declaration_range: start..searcher.pos() as usize,
});
}
}
}
#[must_use]
pub fn read_deprecated_lints() -> (Vec<DeprecatedLint>, Vec<RenamedLint>) {
#[allow(clippy::enum_glob_use)]
use Token::*;
#[rustfmt::skip]
static DECL_TOKENS: &[Token<'_>] = &[
// #[clippy::version = "version"]
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket,
// ("first", "second"),
OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma,
];
#[rustfmt::skip]
static DEPRECATED_TOKENS: &[Token<'_>] = &[
// !{ DEPRECATED(DEPRECATED_VERSION) = [
Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket,
];
#[rustfmt::skip]
static RENAMED_TOKENS: &[Token<'_>] = &[
// !{ RENAMED(RENAMED_VERSION) = [
Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket,
];
let path = "clippy_lints/src/deprecated_lints.rs";
let mut deprecated = Vec::with_capacity(30);
let mut renamed = Vec::with_capacity(80);
let mut contents = String::new();
File::open_read_to_cleared_string(path, &mut contents);
let mut searcher = RustSearcher::new(&contents);
// First instance is the macro definition.
assert!(
searcher.find_token(Ident("declare_with_version")),
"error reading deprecated lints"
);
if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) {
let mut version = "";
let mut name = "";
let mut reason = "";
while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) {
deprecated.push(DeprecatedLint {
name: parse_str_single_line(path.as_ref(), name),
reason: parse_str_single_line(path.as_ref(), reason),
version: parse_str_single_line(path.as_ref(), version),
});
}
} else {
panic!("error reading deprecated lints");
}
if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) {
let mut version = "";
let mut old_name = "";
let mut new_name = "";
while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) {
renamed.push(RenamedLint {
old_name: parse_str_single_line(path.as_ref(), old_name),
new_name: parse_str_single_line(path.as_ref(), new_name),
version: parse_str_single_line(path.as_ref(), version),
});
}
} else {
panic!("error reading renamed lints");
}
deprecated.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
renamed.sort_by(|lhs, rhs| lhs.old_name.cmp(&rhs.old_name));
(deprecated, renamed)
}
/// Removes the line splices and surrounding quotes from a string literal
fn parse_str_lit(s: &str) -> String {
let s = s.strip_prefix("r").unwrap_or(s).trim_matches('#');
let s = s
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
let mut res = String::with_capacity(s.len());
rustc_literal_escaper::unescape_str(s, &mut |_, ch| {
if let Ok(ch) = ch {
res.push(ch);
}
});
res
}
fn parse_str_single_line(path: &Path, s: &str) -> String {
let value = parse_str_lit(s);
assert!(
!value.contains('\n'),
"error parsing `{}`: `{s}` should be a single line string",
path.display(),
);
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_clippy_lint_decls() {
static CONTENTS: &str = r#"
declare_clippy_lint! {
#[clippy::version = "Hello Clippy!"]
pub PTR_ARG,
style,
"really long \
text"
}
declare_clippy_lint!{
#[clippy::version = "Test version"]
pub DOC_MARKDOWN,
pedantic,
"single line"
}
"#;
let mut result = Vec::new();
parse_clippy_lint_decls("".as_ref(), CONTENTS, "module_name", &mut result);
for r in &mut result {
r.declaration_range = Range::default();
}
let expected = vec![
Lint {
name: "ptr_arg".into(),
group: "style".into(),
module: "module_name".into(),
path: PathBuf::new(),
declaration_range: Range::default(),
},
Lint {
name: "doc_markdown".into(),
group: "pedantic".into(),
module: "module_name".into(),
path: PathBuf::new(),
declaration_range: Range::default(),
},
];
assert_eq!(expected, result);
}
}

View file

@ -1,9 +1,9 @@
use core::fmt::{self, Display};
use core::marker::PhantomData;
use core::num::NonZero;
use core::ops::Range;
use core::slice;
use core::ops::{Deref, DerefMut};
use core::range::Range;
use core::str::FromStr;
use rustc_lexer::{self as lexer, FrontmatterAllowed};
use std::ffi::OsStr;
use std::fs::{self, OpenOptions};
use std::io::{self, Read as _, Seek as _, SeekFrom, Write};
@ -12,6 +12,24 @@ use std::process::{self, Command, Stdio};
use std::{env, thread};
use walkdir::WalkDir;
pub struct Scoped<'inner, 'outer: 'inner, T>(T, PhantomData<&'inner mut T>, PhantomData<&'outer mut ()>);
impl<T> Scoped<'_, '_, T> {
pub fn new(value: T) -> Self {
Self(value, PhantomData, PhantomData)
}
}
impl<T> Deref for Scoped<'_, '_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for Scoped<'_, '_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Clone, Copy)]
pub enum ErrAction {
Open,
@ -410,179 +428,6 @@ pub fn update_text_region_fn(
move |path, src, dst| update_text_region(path, start, end, src, dst, &mut insert)
}
#[derive(Clone, Copy)]
pub enum Token<'a> {
/// Matches any number of comments / doc comments.
AnyComment,
Ident(&'a str),
CaptureIdent,
LitStr,
CaptureLitStr,
Bang,
CloseBrace,
CloseBracket,
CloseParen,
/// This will consume the first colon even if the second doesn't exist.
DoubleColon,
Comma,
Eq,
Lifetime,
Lt,
Gt,
OpenBrace,
OpenBracket,
OpenParen,
Pound,
Semi,
}
pub struct RustSearcher<'txt> {
text: &'txt str,
cursor: lexer::Cursor<'txt>,
pos: u32,
next_token: lexer::Token,
}
impl<'txt> RustSearcher<'txt> {
#[must_use]
#[expect(clippy::inconsistent_struct_constructor)]
pub fn new(text: &'txt str) -> Self {
let mut cursor = lexer::Cursor::new(text, FrontmatterAllowed::Yes);
Self {
text,
pos: 0,
next_token: cursor.advance_token(),
cursor,
}
}
#[must_use]
pub fn peek_text(&self) -> &'txt str {
&self.text[self.pos as usize..(self.pos + self.next_token.len) as usize]
}
#[must_use]
pub fn peek_len(&self) -> u32 {
self.next_token.len
}
#[must_use]
pub fn peek(&self) -> lexer::TokenKind {
self.next_token.kind
}
#[must_use]
pub fn pos(&self) -> u32 {
self.pos
}
#[must_use]
pub fn at_end(&self) -> bool {
self.next_token.kind == lexer::TokenKind::Eof
}
pub fn step(&mut self) {
// `next_len` is zero for the sentinel value and the eof marker.
self.pos += self.next_token.len;
self.next_token = self.cursor.advance_token();
}
/// Consumes the next token if it matches the requested value and captures the value if
/// requested. Returns true if a token was matched.
fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool {
loop {
match (token, self.next_token.kind) {
(_, lexer::TokenKind::Whitespace)
| (
Token::AnyComment,
lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. },
) => self.step(),
(Token::AnyComment, _) => return true,
(Token::Bang, lexer::TokenKind::Bang)
| (Token::CloseBrace, lexer::TokenKind::CloseBrace)
| (Token::CloseBracket, lexer::TokenKind::CloseBracket)
| (Token::CloseParen, lexer::TokenKind::CloseParen)
| (Token::Comma, lexer::TokenKind::Comma)
| (Token::Eq, lexer::TokenKind::Eq)
| (Token::Lifetime, lexer::TokenKind::Lifetime { .. })
| (Token::Lt, lexer::TokenKind::Lt)
| (Token::Gt, lexer::TokenKind::Gt)
| (Token::OpenBrace, lexer::TokenKind::OpenBrace)
| (Token::OpenBracket, lexer::TokenKind::OpenBracket)
| (Token::OpenParen, lexer::TokenKind::OpenParen)
| (Token::Pound, lexer::TokenKind::Pound)
| (Token::Semi, lexer::TokenKind::Semi)
| (
Token::LitStr,
lexer::TokenKind::Literal {
kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
..
},
) => {
self.step();
return true;
},
(Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => {
self.step();
return true;
},
(Token::DoubleColon, lexer::TokenKind::Colon) => {
self.step();
if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) {
self.step();
return true;
}
return false;
},
(
Token::CaptureLitStr,
lexer::TokenKind::Literal {
kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
..
},
)
| (Token::CaptureIdent, lexer::TokenKind::Ident) => {
**captures.next().unwrap() = self.peek_text();
self.step();
return true;
},
_ => return false,
}
}
}
#[must_use]
pub fn find_token(&mut self, token: Token<'_>) -> bool {
let mut capture = [].iter_mut();
while !self.read_token(token, &mut capture) {
self.step();
if self.at_end() {
return false;
}
}
true
}
#[must_use]
pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> {
let mut res = "";
let mut capture = &mut res;
let mut capture = slice::from_mut(&mut capture).iter_mut();
while !self.read_token(token, &mut capture) {
self.step();
if self.at_end() {
return None;
}
}
Some(res)
}
#[must_use]
pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool {
let mut captures = captures.iter_mut();
tokens.iter().all(|&t| self.read_token(t, &mut captures))
}
}
#[track_caller]
pub fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
match OpenOptions::new().create_new(true).write(true).open(new_name) {
@ -748,8 +593,8 @@ pub fn delete_dir_if_exists(path: &Path) {
}
/// Walks all items excluding top-level dot files/directories and any target directories.
pub fn walk_dir_no_dot_or_target() -> impl Iterator<Item = ::walkdir::Result<::walkdir::DirEntry>> {
WalkDir::new(".").into_iter().filter_entry(|e| {
pub fn walk_dir_no_dot_or_target(p: impl AsRef<Path>) -> impl Iterator<Item = ::walkdir::Result<::walkdir::DirEntry>> {
WalkDir::new(p).into_iter().filter_entry(|e| {
e.path()
.file_name()
.is_none_or(|x| x != "target" && x.as_encoded_bytes().first().copied() != Some(b'.'))

View file

@ -1,6 +1,6 @@
[package]
name = "clippy_lints"
version = "0.1.92"
version = "0.1.93"
description = "A bunch of helpful lints to avoid common pitfalls in Rust"
repository = "https://github.com/rust-lang/rust-clippy"
readme = "README.md"

View file

@ -135,7 +135,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::duplicate_mod::DUPLICATE_MOD_INFO,
crate::else_if_without_else::ELSE_IF_WITHOUT_ELSE_INFO,
crate::empty_drop::EMPTY_DROP_INFO,
crate::empty_enum::EMPTY_ENUM_INFO,
crate::empty_enums::EMPTY_ENUMS_INFO,
crate::empty_line_after::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO,
crate::empty_line_after::EMPTY_LINE_AFTER_OUTER_ATTR_INFO,
crate::empty_with_brackets::EMPTY_ENUM_VARIANTS_WITH_BRACKETS_INFO,
@ -227,8 +227,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::init_numbered_fields::INIT_NUMBERED_FIELDS_INFO,
crate::inline_fn_without_body::INLINE_FN_WITHOUT_BODY_INFO,
crate::int_plus_one::INT_PLUS_ONE_INFO,
crate::integer_division_remainder_used::INTEGER_DIVISION_REMAINDER_USED_INFO,
crate::invalid_upcast_comparisons::INVALID_UPCAST_COMPARISONS_INFO,
crate::item_name_repetitions::ENUM_VARIANT_NAMES_INFO,
crate::item_name_repetitions::MODULE_INCEPTION_INFO,
crate::item_name_repetitions::MODULE_NAME_REPETITIONS_INFO,
@ -258,7 +256,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::lifetimes::ELIDABLE_LIFETIME_NAMES_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,
@ -298,7 +295,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::manual_async_fn::MANUAL_ASYNC_FN_INFO,
crate::manual_bits::MANUAL_BITS_INFO,
crate::manual_clamp::MANUAL_CLAMP_INFO,
crate::manual_div_ceil::MANUAL_DIV_CEIL_INFO,
crate::manual_float_methods::MANUAL_IS_FINITE_INFO,
crate::manual_float_methods::MANUAL_IS_INFINITE_INFO,
crate::manual_hash_one::MANUAL_HASH_ONE_INFO,
@ -404,6 +400,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::methods::ITER_SKIP_ZERO_INFO,
crate::methods::ITER_WITH_DRAIN_INFO,
crate::methods::JOIN_ABSOLUTE_PATHS_INFO,
crate::methods::LINES_FILTER_MAP_OK_INFO,
crate::methods::MANUAL_CONTAINS_INFO,
crate::methods::MANUAL_C_STR_LITERALS_INFO,
crate::methods::MANUAL_FILTER_MAP_INFO,
@ -545,7 +542,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::needless_continue::NEEDLESS_CONTINUE_INFO,
crate::needless_else::NEEDLESS_ELSE_INFO,
crate::needless_for_each::NEEDLESS_FOR_EACH_INFO,
crate::needless_if::NEEDLESS_IF_INFO,
crate::needless_ifs::NEEDLESS_IFS_INFO,
crate::needless_late_init::NEEDLESS_LATE_INIT_INFO,
crate::needless_maybe_sized::NEEDLESS_MAYBE_SIZED_INFO,
crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO,
@ -592,6 +589,9 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
crate::operators::INTEGER_DIVISION_INFO,
crate::operators::INTEGER_DIVISION_REMAINDER_USED_INFO,
crate::operators::INVALID_UPCAST_COMPARISONS_INFO,
crate::operators::MANUAL_DIV_CEIL_INFO,
crate::operators::MANUAL_IS_MULTIPLE_OF_INFO,
crate::operators::MANUAL_MIDPOINT_INFO,
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,

View file

@ -85,6 +85,8 @@ declare_with_version! { RENAMED(RENAMED_VERSION) = [
("clippy::drop_copy", "dropping_copy_types"),
#[clippy::version = ""]
("clippy::drop_ref", "dropping_references"),
#[clippy::version = "1.92.0"]
("clippy::empty_enum", "clippy::empty_enums"),
#[clippy::version = ""]
("clippy::eval_order_dependence", "clippy::mixed_read_write_in_expression"),
#[clippy::version = "1.53.0"]
@ -137,6 +139,8 @@ declare_with_version! { RENAMED(RENAMED_VERSION) = [
("clippy::mem_discriminant_non_enum", "enum_intrinsics_non_enums"),
#[clippy::version = "1.80.0"]
("clippy::mismatched_target_os", "unexpected_cfgs"),
#[clippy::version = "1.92.0"]
("clippy::needless_if", "clippy::needless_ifs"),
#[clippy::version = ""]
("clippy::new_without_default_derive", "clippy::new_without_default"),
#[clippy::version = ""]

View file

@ -22,7 +22,6 @@ use rustc_resolve::rustdoc::{
};
use rustc_session::impl_lint_pass;
use rustc_span::Span;
use rustc_span::edition::Edition;
use std::ops::Range;
use url::Url;
@ -36,6 +35,7 @@ mod markdown;
mod missing_headers;
mod needless_doctest_main;
mod suspicious_doc_comments;
mod test_attr_in_doctest;
mod too_long_first_doc_paragraph;
declare_clippy_lint! {
@ -900,8 +900,6 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
))
}
const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"];
enum Container {
Blockquote,
List(usize),
@ -966,6 +964,70 @@ fn check_for_code_clusters<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a
}
}
#[derive(Clone, Copy)]
#[expect(clippy::struct_excessive_bools)]
struct CodeTags {
no_run: bool,
ignore: bool,
compile_fail: bool,
rust: bool,
}
impl Default for CodeTags {
fn default() -> Self {
Self {
no_run: false,
ignore: false,
compile_fail: false,
rust: true,
}
}
}
impl CodeTags {
/// Based on <https://github.com/rust-lang/rust/blob/1.90.0/src/librustdoc/html/markdown.rs#L1169>
fn parse(lang: &str) -> Self {
let mut tags = Self::default();
let mut seen_rust_tags = false;
let mut seen_other_tags = false;
for item in lang.split([',', ' ', '\t']) {
match item.trim() {
"" => {},
"rust" => {
tags.rust = true;
seen_rust_tags = true;
},
"ignore" => {
tags.ignore = true;
seen_rust_tags = !seen_other_tags;
},
"no_run" => {
tags.no_run = true;
seen_rust_tags = !seen_other_tags;
},
"should_panic" => seen_rust_tags = !seen_other_tags,
"compile_fail" => {
tags.compile_fail = true;
seen_rust_tags = !seen_other_tags || seen_rust_tags;
},
"test_harness" | "standalone_crate" => {
seen_rust_tags = !seen_other_tags || seen_rust_tags;
},
_ if item.starts_with("ignore-") => seen_rust_tags = true,
_ if item.starts_with("edition") => {},
_ => seen_other_tags = true,
}
}
tags.rust &= seen_rust_tags || !seen_other_tags;
tags
}
}
/// Checks parsed documentation.
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
/// so lints here will generally access that information.
@ -981,14 +1043,10 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
) -> DocHeaders {
// true if a safety header was found
let mut headers = DocHeaders::default();
let mut in_code = false;
let mut code = None;
let mut in_link = None;
let mut in_heading = false;
let mut in_footnote_definition = false;
let mut is_rust = false;
let mut no_test = false;
let mut ignore = false;
let mut edition = None;
let mut ticks_unbalanced = false;
let mut text_to_check: Vec<(CowStr<'_>, Range<usize>, isize)> = Vec::new();
let mut paragraph_range = 0..0;
@ -1048,31 +1106,12 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
containers.pop();
},
Start(CodeBlock(ref kind)) => {
in_code = true;
if let CodeBlockKind::Fenced(lang) = kind {
for item in lang.split(',') {
if item == "ignore" {
is_rust = false;
break;
} else if item == "no_test" {
no_test = true;
} else if item == "no_run" || item == "compile_fail" {
ignore = true;
}
if let Some(stripped) = item.strip_prefix("edition") {
is_rust = true;
edition = stripped.parse::<Edition>().ok();
} else if item.is_empty() || RUST_CODE.contains(&item) {
is_rust = true;
}
}
}
},
End(TagEnd::CodeBlock) => {
in_code = false;
is_rust = false;
ignore = false;
code = Some(match kind {
CodeBlockKind::Indented => CodeTags::default(),
CodeBlockKind::Fenced(lang) => CodeTags::parse(lang),
});
},
End(TagEnd::CodeBlock) => code = None,
Start(Link { dest_url, .. }) => in_link = Some(dest_url),
End(TagEnd::Link) => in_link = None,
Start(Heading { .. } | Paragraph | Item) => {
@ -1182,7 +1221,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
paragraph_range.end = range.end;
let range_ = range.clone();
ticks_unbalanced |= text.contains('`')
&& !in_code
&& code.is_none()
&& doc[range.clone()].bytes().enumerate().any(|(i, c)| {
// scan the markdown source code bytes for backquotes that aren't preceded by backslashes
// - use bytes, instead of chars, to avoid utf8 decoding overhead (special chars are ascii)
@ -1205,10 +1244,14 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
headers.safety |= in_heading && trimmed_text == "Implementation Safety";
headers.errors |= in_heading && trimmed_text == "Errors";
headers.panics |= in_heading && trimmed_text == "Panics";
if in_code {
if is_rust && !no_test {
let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());
needless_doctest_main::check(cx, &text, edition, range.clone(), fragments, ignore);
if let Some(tags) = code {
if tags.rust && !tags.compile_fail && !tags.ignore {
needless_doctest_main::check(cx, &text, range.start, fragments);
if !tags.no_run {
test_attr_in_doctest::check(cx, &text, range.start, fragments);
}
}
} else {
if in_link.is_some() {

View file

@ -1,160 +1,63 @@
use std::ops::Range;
use std::sync::Arc;
use std::{io, thread};
use crate::doc::{NEEDLESS_DOCTEST_MAIN, TEST_ATTR_IN_DOCTEST};
use clippy_utils::diagnostics::span_lint;
use rustc_ast::{CoroutineKind, Fn, FnRetTy, Item, ItemKind};
use rustc_errors::emitter::HumanEmitter;
use rustc_errors::{AutoStream, Diag, DiagCtxt};
use rustc_lint::LateContext;
use rustc_parse::lexer::StripTokens;
use rustc_parse::new_parser_from_source_str;
use rustc_parse::parser::ForceCollect;
use rustc_session::parse::ParseSess;
use rustc_span::edition::Edition;
use rustc_span::source_map::{FilePathMapping, SourceMap};
use rustc_span::{FileName, Ident, Pos, sym};
use super::Fragments;
use crate::doc::NEEDLESS_DOCTEST_MAIN;
use clippy_utils::diagnostics::span_lint;
use clippy_utils::tokenize_with_text;
use rustc_lexer::TokenKind;
use rustc_lint::LateContext;
use rustc_span::InnerSpan;
fn get_test_spans(item: &Item, ident: Ident, test_attr_spans: &mut Vec<Range<usize>>) {
test_attr_spans.extend(
item.attrs
.iter()
.find(|attr| attr.has_name(sym::test))
.map(|attr| attr.span.lo().to_usize()..ident.span.hi().to_usize()),
);
fn returns_unit<'a>(mut tokens: impl Iterator<Item = (TokenKind, &'a str, InnerSpan)>) -> bool {
let mut next = || tokens.next().map_or(TokenKind::Whitespace, |(kind, ..)| kind);
match next() {
// {
TokenKind::OpenBrace => true,
// - > ( ) {
TokenKind::Minus => {
next() == TokenKind::Gt
&& next() == TokenKind::OpenParen
&& next() == TokenKind::CloseParen
&& next() == TokenKind::OpenBrace
},
_ => false,
}
}
pub fn check(
cx: &LateContext<'_>,
text: &str,
edition: Edition,
range: Range<usize>,
fragments: Fragments<'_>,
ignore: bool,
) {
// return whether the code contains a needless `fn main` plus a vector of byte position ranges
// of all `#[test]` attributes in not ignored code examples
fn check_code_sample(code: String, edition: Edition, ignore: bool) -> (bool, Vec<Range<usize>>) {
rustc_driver::catch_fatal_errors(|| {
rustc_span::create_session_globals_then(edition, &[], None, || {
let mut test_attr_spans = vec![];
let filename = FileName::anon_source_code(&code);
let translator = rustc_driver::default_translator();
let emitter = HumanEmitter::new(AutoStream::never(Box::new(io::sink())), translator);
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
#[expect(clippy::arc_with_non_send_sync)] // `Arc` is expected by with_dcx
let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
let psess = ParseSess::with_dcx(dcx, sm);
let mut parser =
match new_parser_from_source_str(&psess, filename, code, StripTokens::ShebangAndFrontmatter) {
Ok(p) => p,
Err(errs) => {
errs.into_iter().for_each(Diag::cancel);
return (false, test_attr_spans);
},
};
let mut relevant_main_found = false;
let mut eligible = true;
loop {
match parser.parse_item(ForceCollect::No) {
Ok(Some(item)) => match &item.kind {
ItemKind::Fn(box Fn {
ident,
sig,
body: Some(block),
..
}) if ident.name == sym::main => {
if !ignore {
get_test_spans(&item, *ident, &mut test_attr_spans);
}
let is_async = matches!(sig.header.coroutine_kind, Some(CoroutineKind::Async { .. }));
let returns_nothing = match &sig.decl.output {
FnRetTy::Default(..) => true,
FnRetTy::Ty(ty) if ty.kind.is_unit() => true,
FnRetTy::Ty(_) => false,
};
if returns_nothing && !is_async && !block.stmts.is_empty() {
// This main function should be linted, but only if there are no other functions
relevant_main_found = true;
} else {
// This main function should not be linted, we're done
eligible = false;
}
},
// Another function was found; this case is ignored for needless_doctest_main
ItemKind::Fn(fn_) => {
eligible = false;
if ignore {
// If ignore is active invalidating one lint,
// and we already found another function thus
// invalidating the other one, we have no
// business continuing.
return (false, test_attr_spans);
}
get_test_spans(&item, fn_.ident, &mut test_attr_spans);
},
// Tests with one of these items are ignored
ItemKind::Static(..)
| ItemKind::Const(..)
| ItemKind::ExternCrate(..)
| ItemKind::ForeignMod(..) => {
eligible = false;
},
_ => {},
},
Ok(None) => break,
Err(e) => {
// See issue #15041. When calling `.cancel()` on the `Diag`, Clippy will unexpectedly panic
// when the `Diag` is unwinded. Meanwhile, we can just call `.emit()`, since the `DiagCtxt`
// is just a sink, nothing will be printed.
e.emit();
return (false, test_attr_spans);
},
}
}
(relevant_main_found & eligible, test_attr_spans)
})
})
.ok()
.unwrap_or_default()
}
let trailing_whitespace = text.len() - text.trim_end().len();
// We currently only test for "fn main". Checking for the real
// entrypoint (with tcx.entry_fn(())) in each block would be unnecessarily
// expensive, as those are probably intended and relevant. Same goes for
// macros and other weird ways of declaring a main function.
//
// Also, as we only check for attribute names and don't do macro expansion,
// we can check only for #[test]
if !((text.contains("main") && text.contains("fn")) || text.contains("#[test]")) {
pub fn check(cx: &LateContext<'_>, text: &str, offset: usize, fragments: Fragments<'_>) {
if !text.contains("main") {
return;
}
// Because of the global session, we need to create a new session in a different thread with
// the edition we need.
let text = text.to_owned();
let (has_main, test_attr_spans) = thread::spawn(move || check_code_sample(text, edition, ignore))
.join()
.expect("thread::spawn failed");
if has_main && let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace) {
span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
}
for span in test_attr_spans {
let span = (range.start + span.start)..(range.start + span.end);
if let Some(span) = fragments.span(cx, span) {
span_lint(cx, TEST_ATTR_IN_DOCTEST, span, "unit tests in doctest are not executed");
let mut tokens = tokenize_with_text(text).filter(|&(kind, ..)| {
!matches!(
kind,
TokenKind::Whitespace | TokenKind::BlockComment { .. } | TokenKind::LineComment { .. }
)
});
if let Some((TokenKind::Ident, "fn", fn_span)) = tokens.next()
&& let Some((TokenKind::Ident, "main", main_span)) = tokens.next()
&& let Some((TokenKind::OpenParen, ..)) = tokens.next()
&& let Some((TokenKind::CloseParen, ..)) = tokens.next()
&& returns_unit(&mut tokens)
{
let mut depth = 1;
for (kind, ..) in &mut tokens {
match kind {
TokenKind::OpenBrace => depth += 1,
TokenKind::CloseBrace => {
depth -= 1;
if depth == 0 {
break;
}
},
_ => {},
}
}
if tokens.next().is_none()
&& let Some(span) = fragments.span(cx, fn_span.start + offset..main_span.end + offset)
{
span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
}
}
}

View file

@ -0,0 +1,34 @@
use super::Fragments;
use crate::doc::TEST_ATTR_IN_DOCTEST;
use clippy_utils::diagnostics::span_lint;
use clippy_utils::tokenize_with_text;
use rustc_lexer::TokenKind;
use rustc_lint::LateContext;
pub fn check(cx: &LateContext<'_>, text: &str, offset: usize, fragments: Fragments<'_>) {
if !text.contains("#[test]") {
return;
}
let mut spans = Vec::new();
let mut tokens = tokenize_with_text(text).filter(|&(kind, ..)| kind != TokenKind::Whitespace);
while let Some(token) = tokens.next() {
if let (TokenKind::Pound, _, pound_span) = token
&& let Some((TokenKind::OpenBracket, ..)) = tokens.next()
&& let Some((TokenKind::Ident, "test", _)) = tokens.next()
&& let Some((TokenKind::CloseBracket, _, close_span)) = tokens.next()
&& let Some(span) = fragments.span(cx, pound_span.start + offset..close_span.end + offset)
{
spans.push(span);
}
}
if !spans.is_empty() {
span_lint(
cx,
TEST_ATTR_IN_DOCTEST,
spans,
"unit tests in doctest are not executed",
);
}
}

View file

@ -1,5 +1,5 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::{HasSession, snippet_with_applicability, snippet_with_context};
use clippy_utils::source::{HasSession, SpanRangeExt, snippet_with_applicability, snippet_with_context};
use rustc_ast::ast::{Expr, ExprKind, MethodCall};
use rustc_errors::Applicability;
use rustc_lint::{EarlyContext, EarlyLintPass};
@ -47,8 +47,12 @@ impl EarlyLintPass for DoubleParens {
// ^^^^^^ expr
// ^^^^ inner
ExprKind::Paren(inner) if matches!(inner.kind, ExprKind::Paren(_) | ExprKind::Tup(_)) => {
// suggest removing the outer parens
if expr.span.eq_ctxt(inner.span) {
if expr.span.eq_ctxt(inner.span)
&& !expr.span.in_external_macro(cx.sess().source_map())
&& check_source(cx, inner)
{
// suggest removing the outer parens
let mut applicability = Applicability::MachineApplicable;
// We don't need to use `snippet_with_context` here, because:
// - if `inner`'s `ctxt` is from macro, we don't lint in the first place (see the check above)
@ -74,8 +78,12 @@ impl EarlyLintPass for DoubleParens {
if let [arg] = &**args
&& let ExprKind::Paren(inner) = &arg.kind =>
{
// suggest removing the inner parens
if expr.span.eq_ctxt(arg.span) {
if expr.span.eq_ctxt(arg.span)
&& !arg.span.in_external_macro(cx.sess().source_map())
&& check_source(cx, arg)
{
// suggest removing the inner parens
let mut applicability = Applicability::MachineApplicable;
let sugg = snippet_with_context(cx.sess(), inner.span, arg.span.ctxt(), "_", &mut applicability).0;
span_lint_and_sugg(
@ -93,3 +101,22 @@ impl EarlyLintPass for DoubleParens {
}
}
}
/// Check that the span does indeed look like `( (..) )`
fn check_source(cx: &EarlyContext<'_>, inner: &Expr) -> bool {
if let Some(sfr) = inner.span.get_source_range(cx)
// this is the same as `SourceFileRange::as_str`, but doesn't apply the range right away, because
// we're interested in the source code outside it
&& let Some(src) = sfr.sf.src.as_ref().map(|src| src.as_str())
&& let Some((start, outer_after_inner)) = src.split_at_checked(sfr.range.end)
&& let Some((outer_before_inner, inner)) = start.split_at_checked(sfr.range.start)
&& outer_before_inner.trim_end().ends_with('(')
&& inner.starts_with('(')
&& inner.ends_with(')')
&& outer_after_inner.trim_start().starts_with(')')
{
true
} else {
false
}
}

View file

@ -1,4 +1,5 @@
use clippy_utils::diagnostics::span_lint_and_help;
use clippy_utils::span_contains_cfg;
use rustc_hir::{Item, ItemKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
@ -25,10 +26,6 @@ declare_clippy_lint! {
/// matched to mark code unreachable. If the field is not visible, then the struct
/// acts like any other struct with private fields.
///
/// * If the enum has no variants only because all variants happen to be
/// [disabled by conditional compilation][cfg], then it would be appropriate
/// to allow the lint, with `#[allow(empty_enum)]`.
///
/// For further information, visit
/// [the never types documentation][`!`].
///
@ -53,24 +50,24 @@ declare_clippy_lint! {
/// [newtype]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#using-the-newtype-pattern-for-type-safety-and-abstraction
/// [visibility]: https://doc.rust-lang.org/reference/visibility-and-privacy.html
#[clippy::version = "pre 1.29.0"]
pub EMPTY_ENUM,
pub EMPTY_ENUMS,
pedantic,
"enum with no variants"
}
declare_lint_pass!(EmptyEnum => [EMPTY_ENUM]);
declare_lint_pass!(EmptyEnums => [EMPTY_ENUMS]);
impl LateLintPass<'_> for EmptyEnum {
impl LateLintPass<'_> for EmptyEnums {
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
if let ItemKind::Enum(..) = item.kind
if let ItemKind::Enum(.., def) = item.kind
&& def.variants.is_empty()
// Only suggest the `never_type` if the feature is enabled
&& cx.tcx.features().never_type()
&& let Some(adt) = cx.tcx.type_of(item.owner_id).instantiate_identity().ty_adt_def()
&& adt.variants().is_empty()
&& !span_contains_cfg(cx, item.span)
{
span_lint_and_help(
cx,
EMPTY_ENUM,
EMPTY_ENUMS,
item.span,
"enum with no variants",
None,

View file

@ -157,6 +157,11 @@ impl<'cx, 'tcx> TypeWalker<'cx, 'tcx> {
let spans = if explicit_params.len() == extra_params.len() {
vec![self.generics.span] // Remove the entire list of generics
} else {
// 1. Start from the last extra param
// 2. While the params preceding it are also extra, construct spans going from the current param to
// the comma before it
// 3. Once this chain of extra params stops, switch to constructing spans going from the current
// param to the comma _after_ it
let mut end: Option<LocalDefId> = None;
extra_params
.iter()

View file

@ -110,7 +110,7 @@ declare_clippy_lint! {
/// } if bar { // looks like an `else` is missing here
/// }
/// ```
#[clippy::version = "1.90.0"]
#[clippy::version = "1.91.0"]
pub POSSIBLE_MISSING_ELSE,
suspicious,
"possibly missing `else`"

View file

@ -3,10 +3,9 @@ use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::msrvs::Msrv;
use clippy_utils::{is_in_const_context, is_in_test};
use rustc_data_structures::fx::FxHashMap;
use rustc_hir::def::DefKind;
use rustc_hir::{self as hir, AmbigArg, Expr, ExprKind, HirId, RustcVersion, StabilityLevel, StableSince};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::TyCtxt;
use rustc_middle::ty::{self, TyCtxt};
use rustc_session::impl_lint_pass;
use rustc_span::def_id::{CrateNum, DefId};
use rustc_span::{ExpnKind, Span, sym};
@ -83,6 +82,10 @@ pub struct IncompatibleMsrv {
availability_cache: FxHashMap<(DefId, bool), Availability>,
check_in_tests: bool,
core_crate: Option<CrateNum>,
// The most recently called path. Used to skip checking the path after it's
// been checked when visiting the call expression.
called_path: Option<HirId>,
}
impl_lint_pass!(IncompatibleMsrv => [INCOMPATIBLE_MSRV]);
@ -98,6 +101,7 @@ impl IncompatibleMsrv {
.iter()
.find(|krate| tcx.crate_name(**krate) == sym::core)
.copied(),
called_path: None,
}
}
@ -140,7 +144,14 @@ impl IncompatibleMsrv {
}
/// Emit lint if `def_id`, associated with `node` and `span`, is below the current MSRV.
fn emit_lint_if_under_msrv(&mut self, cx: &LateContext<'_>, def_id: DefId, node: HirId, span: Span) {
fn emit_lint_if_under_msrv(
&mut self,
cx: &LateContext<'_>,
needs_const: bool,
def_id: DefId,
node: HirId,
span: Span,
) {
if def_id.is_local() {
// We don't check local items since their MSRV is supposed to always be valid.
return;
@ -158,10 +169,6 @@ impl IncompatibleMsrv {
return;
}
let needs_const = cx.enclosing_body.is_some()
&& is_in_const_context(cx)
&& matches!(cx.tcx.def_kind(def_id), DefKind::AssocFn | DefKind::Fn);
if (self.check_in_tests || !is_in_test(cx.tcx, node))
&& let Some(current) = self.msrv.current(cx)
&& let Availability::Since(version) = self.get_def_id_availability(cx.tcx, def_id, needs_const)
@ -190,13 +197,33 @@ impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv {
match expr.kind {
ExprKind::MethodCall(_, _, _, span) => {
if let Some(method_did) = cx.typeck_results().type_dependent_def_id(expr.hir_id) {
self.emit_lint_if_under_msrv(cx, method_did, expr.hir_id, span);
self.emit_lint_if_under_msrv(cx, is_in_const_context(cx), method_did, expr.hir_id, span);
}
},
ExprKind::Path(qpath) => {
if let Some(path_def_id) = cx.qpath_res(&qpath, expr.hir_id).opt_def_id() {
self.emit_lint_if_under_msrv(cx, path_def_id, expr.hir_id, expr.span);
}
ExprKind::Call(callee, _) if let ExprKind::Path(qpath) = callee.kind => {
self.called_path = Some(callee.hir_id);
let needs_const = is_in_const_context(cx);
let def_id = if let Some(def_id) = cx.qpath_res(&qpath, callee.hir_id).opt_def_id() {
def_id
} else if needs_const && let ty::FnDef(def_id, _) = *cx.typeck_results().expr_ty(callee).kind() {
// Edge case where a function is first assigned then called.
// We previously would have warned for the non-const MSRV, when
// checking the path, but now that it's called the const MSRV
// must also be met.
def_id
} else {
return;
};
self.emit_lint_if_under_msrv(cx, needs_const, def_id, expr.hir_id, callee.span);
},
// Desugaring into function calls by the compiler will use `QPath::LangItem` variants. Those should
// not be linted as they will not be generated in older compilers if the function is not available,
// and the compiler is allowed to call unstable functions.
ExprKind::Path(qpath)
if let Some(path_def_id) = cx.qpath_res(&qpath, expr.hir_id).opt_def_id()
&& self.called_path != Some(expr.hir_id) =>
{
self.emit_lint_if_under_msrv(cx, false, path_def_id, expr.hir_id, expr.span);
},
_ => {},
}
@ -208,7 +235,7 @@ impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv {
// `CStr` and `CString` have been moved around but have been available since Rust 1.0.0
&& !matches!(cx.tcx.get_diagnostic_name(ty_def_id), Some(sym::cstr_type | sym::cstring_type))
{
self.emit_lint_if_under_msrv(cx, ty_def_id, hir_ty.hir_id, hir_ty.span);
self.emit_lint_if_under_msrv(cx, false, ty_def_id, hir_ty.hir_id, hir_ty.span);
}
}
}

View file

@ -1,50 +0,0 @@
use clippy_utils::diagnostics::span_lint;
use rustc_ast::BinOpKind;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self};
use rustc_session::declare_lint_pass;
declare_clippy_lint! {
/// ### What it does
/// Checks for the usage of division (`/`) and remainder (`%`) operations
/// when performed on any integer types using the default `Div` and `Rem` trait implementations.
///
/// ### Why restrict this?
/// In cryptographic contexts, division can result in timing sidechannel vulnerabilities,
/// and needs to be replaced with constant-time code instead (e.g. Barrett reduction).
///
/// ### Example
/// ```no_run
/// let my_div = 10 / 2;
/// ```
/// Use instead:
/// ```no_run
/// let my_div = 10 >> 1;
/// ```
#[clippy::version = "1.79.0"]
pub INTEGER_DIVISION_REMAINDER_USED,
restriction,
"use of disallowed default division and remainder operations"
}
declare_lint_pass!(IntegerDivisionRemainderUsed => [INTEGER_DIVISION_REMAINDER_USED]);
impl LateLintPass<'_> for IntegerDivisionRemainderUsed {
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
if let ExprKind::Binary(op, lhs, rhs) = &expr.kind
&& let BinOpKind::Div | BinOpKind::Rem = op.node
&& let lhs_ty = cx.typeck_results().expr_ty(lhs)
&& let rhs_ty = cx.typeck_results().expr_ty(rhs)
&& let ty::Int(_) | ty::Uint(_) = lhs_ty.peel_refs().kind()
&& let ty::Int(_) | ty::Uint(_) = rhs_ty.peel_refs().kind()
{
span_lint(
cx,
INTEGER_DIVISION_REMAINDER_USED,
expr.span.source_callsite(),
format!("use of {} has been disallowed in this context", op.node.as_str()),
);
}
}
}

View file

@ -1,4 +1,6 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then};
use clippy_utils::msrvs::Msrv;
use clippy_utils::res::{MaybeDef, MaybeTypeckRes};
use clippy_utils::source::{SpanRangeExt, snippet_with_context};
use clippy_utils::sugg::{Sugg, has_enclosing_paren};
@ -10,12 +12,12 @@ use rustc_hir::def::Res;
use rustc_hir::def_id::{DefId, DefIdSet};
use rustc_hir::{
BinOpKind, Expr, ExprKind, FnRetTy, GenericArg, GenericBound, HirId, ImplItem, ImplItemKind, ImplicitSelfKind,
Item, ItemKind, Mutability, Node, OpaqueTyOrigin, PatExprKind, PatKind, PathSegment, PrimTy, QPath, TraitItemId,
TyKind,
Item, ItemKind, Mutability, Node, OpaqueTyOrigin, PatExprKind, PatKind, PathSegment, PrimTy, QPath, RustcVersion,
StabilityLevel, StableSince, TraitItemId, TyKind,
};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, FnSig, Ty};
use rustc_session::declare_lint_pass;
use rustc_session::impl_lint_pass;
use rustc_span::source_map::Spanned;
use rustc_span::symbol::kw;
use rustc_span::{Ident, Span, Symbol};
@ -120,7 +122,17 @@ declare_clippy_lint! {
"checking `x == \"\"` or `x == []` (or similar) when `.is_empty()` could be used instead"
}
declare_lint_pass!(LenZero => [LEN_ZERO, LEN_WITHOUT_IS_EMPTY, COMPARISON_TO_EMPTY]);
pub struct LenZero {
msrv: Msrv,
}
impl_lint_pass!(LenZero => [LEN_ZERO, LEN_WITHOUT_IS_EMPTY, COMPARISON_TO_EMPTY]);
impl LenZero {
pub fn new(conf: &'static Conf) -> Self {
Self { msrv: conf.msrv }
}
}
impl<'tcx> LateLintPass<'tcx> for LenZero {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
@ -184,7 +196,7 @@ impl<'tcx> LateLintPass<'tcx> for LenZero {
_ => false,
}
&& !expr.span.from_expansion()
&& has_is_empty(cx, lt.init)
&& has_is_empty(cx, lt.init, self.msrv)
{
let mut applicability = Applicability::MachineApplicable;
@ -206,7 +218,7 @@ impl<'tcx> LateLintPass<'tcx> for LenZero {
&& cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::PartialEq)
&& !expr.span.from_expansion()
{
check_empty_expr(
self.check_empty_expr(
cx,
expr.span,
lhs_expr,
@ -226,29 +238,110 @@ impl<'tcx> LateLintPass<'tcx> for LenZero {
let actual_span = span_without_enclosing_paren(cx, expr.span);
match cmp {
BinOpKind::Eq => {
check_cmp(cx, actual_span, left, right, "", 0); // len == 0
check_cmp(cx, actual_span, right, left, "", 0); // 0 == len
self.check_cmp(cx, actual_span, left, right, "", 0); // len == 0
self.check_cmp(cx, actual_span, right, left, "", 0); // 0 == len
},
BinOpKind::Ne => {
check_cmp(cx, actual_span, left, right, "!", 0); // len != 0
check_cmp(cx, actual_span, right, left, "!", 0); // 0 != len
self.check_cmp(cx, actual_span, left, right, "!", 0); // len != 0
self.check_cmp(cx, actual_span, right, left, "!", 0); // 0 != len
},
BinOpKind::Gt => {
check_cmp(cx, actual_span, left, right, "!", 0); // len > 0
check_cmp(cx, actual_span, right, left, "", 1); // 1 > len
self.check_cmp(cx, actual_span, left, right, "!", 0); // len > 0
self.check_cmp(cx, actual_span, right, left, "", 1); // 1 > len
},
BinOpKind::Lt => {
check_cmp(cx, actual_span, left, right, "", 1); // len < 1
check_cmp(cx, actual_span, right, left, "!", 0); // 0 < len
self.check_cmp(cx, actual_span, left, right, "", 1); // len < 1
self.check_cmp(cx, actual_span, right, left, "!", 0); // 0 < len
},
BinOpKind::Ge => check_cmp(cx, actual_span, left, right, "!", 1), // len >= 1
BinOpKind::Le => check_cmp(cx, actual_span, right, left, "!", 1), // 1 <= len
BinOpKind::Ge => self.check_cmp(cx, actual_span, left, right, "!", 1), // len >= 1
BinOpKind::Le => self.check_cmp(cx, actual_span, right, left, "!", 1), // 1 <= len
_ => (),
}
}
}
}
impl LenZero {
fn check_cmp(
&self,
cx: &LateContext<'_>,
span: Span,
method: &Expr<'_>,
lit: &Expr<'_>,
op: &str,
compare_to: u32,
) {
if method.span.from_expansion() {
return;
}
if let (&ExprKind::MethodCall(method_path, receiver, [], _), ExprKind::Lit(lit)) = (&method.kind, &lit.kind) {
// check if we are in an is_empty() method
if parent_item_name(cx, method) == Some(sym::is_empty) {
return;
}
self.check_len(cx, span, method_path.ident.name, receiver, &lit.node, op, compare_to);
} else {
self.check_empty_expr(cx, span, method, lit, op);
}
}
#[expect(clippy::too_many_arguments)]
fn check_len(
&self,
cx: &LateContext<'_>,
span: Span,
method_name: Symbol,
receiver: &Expr<'_>,
lit: &LitKind,
op: &str,
compare_to: u32,
) {
if let LitKind::Int(lit, _) = *lit {
// check if length is compared to the specified number
if lit != u128::from(compare_to) {
return;
}
if method_name == sym::len && has_is_empty(cx, receiver, self.msrv) {
let mut applicability = Applicability::MachineApplicable;
span_lint_and_sugg(
cx,
LEN_ZERO,
span,
format!("length comparison to {}", if compare_to == 0 { "zero" } else { "one" }),
format!("using `{op}is_empty` is clearer and more explicit"),
format!(
"{op}{}.is_empty()",
snippet_with_context(cx, receiver.span, span.ctxt(), "_", &mut applicability).0,
),
applicability,
);
}
}
}
fn check_empty_expr(&self, cx: &LateContext<'_>, span: Span, lit1: &Expr<'_>, lit2: &Expr<'_>, op: &str) {
if (is_empty_array(lit2) || is_empty_string(lit2)) && has_is_empty(cx, lit1, self.msrv) {
let mut applicability = Applicability::MachineApplicable;
let lit1 = peel_ref_operators(cx, lit1);
let lit_str = Sugg::hir_with_context(cx, lit1, span.ctxt(), "_", &mut applicability).maybe_paren();
span_lint_and_sugg(
cx,
COMPARISON_TO_EMPTY,
span,
"comparison to empty slice",
format!("using `{op}is_empty` is clearer and more explicit"),
format!("{op}{lit_str}.is_empty()"),
applicability,
);
}
}
}
fn span_without_enclosing_paren(cx: &LateContext<'_>, span: Span) -> Span {
let Some(snippet) = span.get_source_text(cx) else {
return span;
@ -513,75 +606,6 @@ fn check_for_is_empty(
}
}
fn check_cmp(cx: &LateContext<'_>, span: Span, method: &Expr<'_>, lit: &Expr<'_>, op: &str, compare_to: u32) {
if method.span.from_expansion() {
return;
}
if let (&ExprKind::MethodCall(method_path, receiver, [], _), ExprKind::Lit(lit)) = (&method.kind, &lit.kind) {
// check if we are in an is_empty() method
if parent_item_name(cx, method) == Some(sym::is_empty) {
return;
}
check_len(cx, span, method_path.ident.name, receiver, &lit.node, op, compare_to);
} else {
check_empty_expr(cx, span, method, lit, op);
}
}
fn check_len(
cx: &LateContext<'_>,
span: Span,
method_name: Symbol,
receiver: &Expr<'_>,
lit: &LitKind,
op: &str,
compare_to: u32,
) {
if let LitKind::Int(lit, _) = *lit {
// check if length is compared to the specified number
if lit != u128::from(compare_to) {
return;
}
if method_name == sym::len && has_is_empty(cx, receiver) {
let mut applicability = Applicability::MachineApplicable;
span_lint_and_sugg(
cx,
LEN_ZERO,
span,
format!("length comparison to {}", if compare_to == 0 { "zero" } else { "one" }),
format!("using `{op}is_empty` is clearer and more explicit"),
format!(
"{op}{}.is_empty()",
snippet_with_context(cx, receiver.span, span.ctxt(), "_", &mut applicability).0,
),
applicability,
);
}
}
}
fn check_empty_expr(cx: &LateContext<'_>, span: Span, lit1: &Expr<'_>, lit2: &Expr<'_>, op: &str) {
if (is_empty_array(lit2) || is_empty_string(lit2)) && has_is_empty(cx, lit1) {
let mut applicability = Applicability::MachineApplicable;
let lit1 = peel_ref_operators(cx, lit1);
let lit_str = Sugg::hir_with_context(cx, lit1, span.ctxt(), "_", &mut applicability).maybe_paren();
span_lint_and_sugg(
cx,
COMPARISON_TO_EMPTY,
span,
"comparison to empty slice",
format!("using `{op}is_empty` is clearer and more explicit"),
format!("{op}{lit_str}.is_empty()"),
applicability,
);
}
}
fn is_empty_string(expr: &Expr<'_>) -> bool {
if let ExprKind::Lit(lit) = expr.kind
&& let LitKind::Str(lit, _) = lit.node
@ -600,45 +624,59 @@ fn is_empty_array(expr: &Expr<'_>) -> bool {
}
/// Checks if this type has an `is_empty` method.
fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>, msrv: Msrv) -> bool {
/// Gets an `AssocItem` and return true if it matches `is_empty(self)`.
fn is_is_empty(cx: &LateContext<'_>, item: &ty::AssocItem) -> bool {
fn is_is_empty_and_stable(cx: &LateContext<'_>, item: &ty::AssocItem, msrv: Msrv) -> bool {
if item.is_fn() {
let sig = cx.tcx.fn_sig(item.def_id).skip_binder();
let ty = sig.skip_binder();
ty.inputs().len() == 1
&& cx.tcx.lookup_stability(item.def_id).is_none_or(|stability| {
if let StabilityLevel::Stable { since, .. } = stability.level {
let version = match since {
StableSince::Version(version) => version,
StableSince::Current => RustcVersion::CURRENT,
StableSince::Err(_) => return false,
};
msrv.meets(cx, version)
} else {
// Unstable fn, check if the feature is enabled.
cx.tcx.features().enabled(stability.feature) && msrv.current(cx).is_none()
}
})
} else {
false
}
}
/// Checks the inherent impl's items for an `is_empty(self)` method.
fn has_is_empty_impl(cx: &LateContext<'_>, id: DefId) -> bool {
fn has_is_empty_impl(cx: &LateContext<'_>, id: DefId, msrv: Msrv) -> bool {
cx.tcx.inherent_impls(id).iter().any(|imp| {
cx.tcx
.associated_items(*imp)
.filter_by_name_unhygienic(sym::is_empty)
.any(|item| is_is_empty(cx, item))
.any(|item| is_is_empty_and_stable(cx, item, msrv))
})
}
fn ty_has_is_empty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, depth: usize) -> bool {
fn ty_has_is_empty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, depth: usize, msrv: Msrv) -> bool {
match ty.kind() {
ty::Dynamic(tt, ..) => tt.principal().is_some_and(|principal| {
cx.tcx
.associated_items(principal.def_id())
.filter_by_name_unhygienic(sym::is_empty)
.any(|item| is_is_empty(cx, item))
.any(|item| is_is_empty_and_stable(cx, item, msrv))
}),
ty::Alias(ty::Projection, proj) => has_is_empty_impl(cx, proj.def_id),
ty::Alias(ty::Projection, proj) => has_is_empty_impl(cx, proj.def_id, msrv),
ty::Adt(id, _) => {
has_is_empty_impl(cx, id.did())
has_is_empty_impl(cx, id.did(), msrv)
|| (cx.tcx.recursion_limit().value_within_limit(depth)
&& cx.tcx.get_diagnostic_item(sym::Deref).is_some_and(|deref_id| {
implements_trait(cx, ty, deref_id, &[])
&& cx
.get_associated_type(ty, deref_id, sym::Target)
.is_some_and(|deref_ty| ty_has_is_empty(cx, deref_ty, depth + 1))
.is_some_and(|deref_ty| ty_has_is_empty(cx, deref_ty, depth + 1, msrv))
}))
},
ty::Array(..) | ty::Slice(..) | ty::Str => true,
@ -646,5 +684,5 @@ fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
}
}
ty_has_is_empty(cx, cx.typeck_results().expr_ty(expr).peel_refs(), 0)
ty_has_is_empty(cx, cx.typeck_results().expr_ty(expr).peel_refs(), 0, msrv)
}

View file

@ -32,7 +32,6 @@ extern crate rustc_arena;
extern crate rustc_ast;
extern crate rustc_ast_pretty;
extern crate rustc_data_structures;
extern crate rustc_driver;
extern crate rustc_errors;
extern crate rustc_hir;
extern crate rustc_hir_analysis;
@ -43,7 +42,6 @@ extern crate rustc_infer;
extern crate rustc_lexer;
extern crate rustc_lint;
extern crate rustc_middle;
extern crate rustc_parse;
extern crate rustc_parse_format;
extern crate rustc_resolve;
extern crate rustc_session;
@ -117,7 +115,7 @@ mod drop_forget_ref;
mod duplicate_mod;
mod else_if_without_else;
mod empty_drop;
mod empty_enum;
mod empty_enums;
mod empty_line_after;
mod empty_with_brackets;
mod endian_bytes;
@ -171,8 +169,6 @@ mod inherent_to_string;
mod init_numbered_fields;
mod inline_fn_without_body;
mod int_plus_one;
mod integer_division_remainder_used;
mod invalid_upcast_comparisons;
mod item_name_repetitions;
mod items_after_statements;
mod items_after_test_module;
@ -191,7 +187,6 @@ mod let_if_seq;
mod let_underscore;
mod let_with_type_underscore;
mod lifetimes;
mod lines_filter_map_ok;
mod literal_representation;
mod literal_string_with_formatting_args;
mod loops;
@ -203,7 +198,6 @@ mod manual_assert;
mod manual_async_fn;
mod manual_bits;
mod manual_clamp;
mod manual_div_ceil;
mod manual_float_methods;
mod manual_hash_one;
mod manual_ignore_case_cmp;
@ -255,7 +249,7 @@ mod needless_borrows_for_generic_args;
mod needless_continue;
mod needless_else;
mod needless_for_each;
mod needless_if;
mod needless_ifs;
mod needless_late_init;
mod needless_maybe_sized;
mod needless_parens_on_range_literals;
@ -481,7 +475,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(mut_mut::MutMut::default()));
store.register_late_pass(|_| Box::new(unnecessary_mut_passed::UnnecessaryMutPassed));
store.register_late_pass(|_| Box::<significant_drop_tightening::SignificantDropTightening<'_>>::default());
store.register_late_pass(|_| Box::new(len_zero::LenZero));
store.register_late_pass(move |_| Box::new(len_zero::LenZero::new(conf)));
store.register_late_pass(move |_| Box::new(attrs::Attributes::new(conf)));
store.register_late_pass(|_| Box::new(blocks_in_conditions::BlocksInConditions));
store.register_late_pass(|_| Box::new(unicode::Unicode));
@ -542,8 +536,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(derive::Derive));
store.register_late_pass(move |_| Box::new(derivable_impls::DerivableImpls::new(conf)));
store.register_late_pass(|_| Box::new(drop_forget_ref::DropForgetRef));
store.register_late_pass(|_| Box::new(empty_enum::EmptyEnum));
store.register_late_pass(|_| Box::new(invalid_upcast_comparisons::InvalidUpcastComparisons));
store.register_late_pass(|_| Box::new(empty_enums::EmptyEnums));
store.register_late_pass(|_| Box::<regex::Regex>::default());
store.register_late_pass(move |tcx| Box::new(ifs::CopyAndPaste::new(tcx, conf)));
store.register_late_pass(|_| Box::new(copy_iterator::CopyIterator));
@ -743,7 +736,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(move |_| Box::new(manual_main_separator_str::ManualMainSeparatorStr::new(conf)));
store.register_late_pass(|_| Box::new(unnecessary_struct_initialization::UnnecessaryStruct));
store.register_late_pass(move |_| Box::new(unnecessary_box_returns::UnnecessaryBoxReturns::new(conf)));
store.register_late_pass(move |_| Box::new(lines_filter_map_ok::LinesFilterMapOk::new(conf)));
store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule));
store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation::new(conf)));
store.register_early_pass(move || Box::new(excessive_nesting::ExcessiveNesting::new(conf)));
@ -755,7 +747,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(endian_bytes::EndianBytes));
store.register_late_pass(|_| Box::new(redundant_type_annotations::RedundantTypeAnnotations));
store.register_late_pass(|_| Box::new(arc_with_non_send_sync::ArcWithNonSendSync));
store.register_late_pass(|_| Box::new(needless_if::NeedlessIf));
store.register_late_pass(|_| Box::new(needless_ifs::NeedlessIfs));
store.register_late_pass(move |_| Box::new(min_ident_chars::MinIdentChars::new(conf)));
store.register_late_pass(move |_| Box::new(large_stack_frames::LargeStackFrames::new(conf)));
store.register_late_pass(|_| Box::new(single_range_in_vec_init::SingleRangeInVecInit));
@ -798,7 +790,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_early_pass(|| Box::new(multiple_bound_locations::MultipleBoundLocations));
store.register_late_pass(move |_| Box::new(assigning_clones::AssigningClones::new(conf)));
store.register_late_pass(|_| Box::new(zero_repeat_side_effects::ZeroRepeatSideEffects));
store.register_late_pass(|_| Box::new(integer_division_remainder_used::IntegerDivisionRemainderUsed));
store.register_late_pass(move |_| Box::new(macro_metavars_in_unsafe::ExprMetavarsInUnsafe::new(conf)));
store.register_late_pass(move |_| Box::new(string_patterns::StringPatterns::new(conf)));
store.register_early_pass(|| Box::new(field_scoped_visibility_modifiers::FieldScopedVisibilityModifiers));
@ -807,7 +798,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_early_pass(|| Box::new(cfg_not_test::CfgNotTest));
store.register_late_pass(|_| Box::new(zombie_processes::ZombieProcesses));
store.register_late_pass(|_| Box::new(pointers_in_nomem_asm_block::PointersInNomemAsmBlock));
store.register_late_pass(move |_| Box::new(manual_div_ceil::ManualDivCeil::new(conf)));
store.register_late_pass(move |_| Box::new(manual_is_power_of_two::ManualIsPowerOfTwo::new(conf)));
store.register_late_pass(|_| Box::new(non_zero_suggestions::NonZeroSuggestions));
store.register_late_pass(|_| Box::new(literal_string_with_formatting_args::LiteralStringWithFormattingArg));

View file

@ -856,89 +856,49 @@ fn elision_suggestions(
.filter(|param| !param.is_elided_lifetime() && !param.is_impl_trait())
.collect::<Vec<_>>();
if !elidable_lts
.iter()
.all(|lt| explicit_params.iter().any(|param| param.def_id == *lt))
{
return None;
}
let mut suggestions = if elidable_lts.is_empty() {
vec![]
} else if elidable_lts.len() == explicit_params.len() {
let mut suggestions = if elidable_lts.len() == explicit_params.len() {
// if all the params are elided remove the whole generic block
//
// fn x<'a>() {}
// ^^^^
vec![(generics.span, String::new())]
} else {
match &explicit_params[..] {
// no params, nothing to elide
[] => unreachable!("handled by `elidable_lts.is_empty()`"),
[param] => {
if elidable_lts.contains(&param.def_id) {
unreachable!("handled by `elidable_lts.len() == explicit_params.len()`")
// 1. Start from the last elidable lifetime
// 2. While the lifetimes preceding it are also elidable, construct spans going from the current
// lifetime to the comma before it
// 3. Once this chain of elidable lifetimes stops, switch to constructing spans going from the
// current lifetime to the comma _after_ it
let mut end: Option<LocalDefId> = None;
elidable_lts
.iter()
.rev()
.map(|&id| {
let (idx, param) = explicit_params.iter().find_position(|param| param.def_id == id)?;
let span = 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.
// fn x<'prev, 'a, 'next>() {}
// ^^^^
param.span.until(next.span)
} else {
unreachable!("handled by `elidable_lts.is_empty()`")
}
},
[_, _, ..] => {
// Given a list like `<'a, 'b, 'c, 'd, ..>`,
//
// If there is a cluster of elidable lifetimes at the beginning, say `'a` and `'b`, we should
// suggest removing them _and_ the trailing comma. The span for that is `a.span.until(c.span)`:
// <'a, 'b, 'c, 'd, ..> => <'a, 'b, 'c, 'd, ..>
// ^^ ^^ ^^^^^^^^
//
// And since we know that `'c` isn't elidable--otherwise it would've been in the cluster--we can go
// over all the lifetimes after it, and for each elidable one, add a suggestion spanning the
// lifetime itself and the comma before, because each individual suggestion is guaranteed to leave
// the list valid:
// <.., 'c, 'd, 'e, 'f, 'g, ..> => <.., 'c, 'd, 'e, 'f, 'g, ..>
// ^^ ^^ ^^ ^^^^ ^^^^^^^^
//
// In case there is no such starting cluster, we only need to do the second part of the algorithm:
// <'a, 'b, 'c, 'd, 'e, 'f, 'g, ..> => <'a, 'b , 'c, 'd, 'e, 'f, 'g, ..>
// ^^ ^^ ^^ ^^ ^^^^^^^^^ ^^^^^^^^
// 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);
// Split off the starting cluster
// TODO: use `slice::split_once` once stabilized (github.com/rust-lang/rust/issues/112811):
// ```
// let Some(split) = explicit_params.split_once(|param| !elidable_lts.contains(&param.def_id)) else {
// // there were no lifetime param that couldn't be elided
// unreachable!("handled by `elidable_lts.len() == explicit_params.len()`")
// };
// match split { /* .. */ }
// ```
let Some(split_pos) = explicit_params
.iter()
.position(|param| !elidable_lts.contains(&param.def_id))
else {
// there were no lifetime param that couldn't be elided
unreachable!("handled by `elidable_lts.len() == explicit_params.len()`")
// `idx` will never be 0, else we'd be removing the entire list of generics
let prev = explicit_params.get(idx - 1)?;
// fn x<'prev, 'a>() {}
// ^^^^
param.span.with_lo(prev.span.hi())
};
let split = explicit_params
.split_at_checked(split_pos)
.expect("got `split_pos` from `position` on the same Vec");
match split {
([..], []) => unreachable!("handled by `elidable_lts.len() == explicit_params.len()`"),
([], [_]) => unreachable!("handled by `explicit_params.len() == 1`"),
(cluster, rest @ [rest_first, ..]) => {
// the span for the cluster
(cluster.first().map(|fw| fw.span.until(rest_first.span)).into_iter())
// the span for the remaining lifetimes (calculations independent of the cluster)
.chain(
rest.array_windows()
.filter(|[_, curr]| elidable_lts.contains(&curr.def_id))
.map(|[prev, curr]| curr.span.with_lo(prev.span.hi())),
)
.map(|sp| (sp, String::new()))
.collect()
},
}
},
}
Some((span, String::new()))
})
.collect::<Option<Vec<_>>>()?
};
suggestions.extend(usages.iter().map(|&usage| {

View file

@ -1,141 +0,0 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::res::{MaybeDef, MaybeResPath, MaybeTypeckRes};
use clippy_utils::sym;
use rustc_errors::Applicability;
use rustc_hir::{Body, Closure, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::impl_lint_pass;
use rustc_span::Symbol;
pub struct LinesFilterMapOk {
msrv: Msrv,
}
impl LinesFilterMapOk {
pub fn new(conf: &Conf) -> Self {
Self { msrv: conf.msrv }
}
}
declare_clippy_lint! {
/// ### What it does
/// Checks for usage 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 are 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
/// ```no_run
/// # 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:
/// ```no_run
/// # 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()`, `flat_map()`, or `flatten()` might cause an infinite loop"
}
impl_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_args, fm_span) = expr.kind
&& cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::Iterator)
&& let fm_method_name = fm_method.ident.name
&& matches!(fm_method_name, sym::filter_map | sym::flat_map | sym::flatten)
&& cx
.typeck_results()
.expr_ty_adjusted(fm_receiver)
.is_diag_item(cx, sym::IoLines)
&& should_lint(cx, fm_args, fm_method_name)
&& self.msrv.meets(cx, msrvs::MAP_WHILE)
{
span_lint_and_then(
cx,
LINES_FILTER_MAP_OK,
fm_span,
format!("`{fm_method_name}()` will run forever if the iterator repeatedly produces an `Err`",),
|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,
);
},
);
}
}
}
fn should_lint(cx: &LateContext<'_>, args: &[Expr<'_>], method_name: Symbol) -> bool {
match args {
[] => method_name == sym::flatten,
[fm_arg] => {
match &fm_arg.kind {
// Detect `Result::ok`
ExprKind::Path(qpath) => cx
.qpath_res(qpath, fm_arg.hir_id)
.opt_def_id()
.is_some_and(|did| cx.tcx.is_diagnostic_item(sym::result_ok_method, did)),
// 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
{
method.ident.name == sym::ok
&& receiver.res_local_id() == Some(param.pat.hir_id)
&& cx
.typeck_results()
.type_dependent_def_id(value.hir_id)
.opt_parent(cx)
.opt_impl_ty(cx)
.is_diag_item(cx, sym::Result)
} else {
false
}
},
_ => false,
}
},
_ => false,
}
}

View file

@ -157,10 +157,7 @@ pub(super) fn check<'tcx>(
"consider using an iterator and enumerate()",
vec![
(pat.span, format!("({}, <item>)", ident.name)),
(
span,
format!("{indexed}.{method}().enumerate(){method_1}{method_2}"),
),
(span, format!("{indexed}.{method}().enumerate(){method_1}{method_2}")),
],
Applicability::HasPlaceholders,
);

View file

@ -7,7 +7,7 @@ use clippy_utils::source::snippet;
use clippy_utils::visitors::{Descend, for_each_expr_without_closures};
use rustc_errors::Applicability;
use rustc_hir::{
Block, Destination, Expr, ExprKind, HirId, InlineAsmOperand, Node, Pat, Stmt, StmtKind, StructTailExpr,
Block, Destination, Expr, ExprKind, HirId, InlineAsm, InlineAsmOperand, Node, Pat, Stmt, StmtKind, StructTailExpr,
};
use rustc_lint::LateContext;
use rustc_span::{BytePos, Span, sym};
@ -75,12 +75,19 @@ pub(super) fn check<'tcx>(
fn contains_any_break_or_continue(block: &Block<'_>) -> bool {
for_each_expr_without_closures(block, |e| match e.kind {
ExprKind::Break(..) | ExprKind::Continue(..) => ControlFlow::Break(()),
ExprKind::InlineAsm(asm) if contains_label(asm) => ControlFlow::Break(()),
ExprKind::Loop(..) => ControlFlow::Continue(Descend::No),
_ => ControlFlow::Continue(Descend::Yes),
})
.is_some()
}
fn contains_label(asm: &InlineAsm<'_>) -> bool {
asm.operands
.iter()
.any(|(op, _span)| matches!(op, InlineAsmOperand::Label { .. }))
}
/// The `never_loop` analysis keeps track of three things:
///
/// * Has any (reachable) code path hit a `continue` of the main loop?
@ -378,7 +385,15 @@ fn never_loop_expr<'tcx>(
InlineAsmOperand::Const { .. } | InlineAsmOperand::SymFn { .. } | InlineAsmOperand::SymStatic { .. } => {
NeverLoopResult::Normal
},
InlineAsmOperand::Label { block } => never_loop_block(cx, block, local_labels, main_loop_id),
InlineAsmOperand::Label { block } =>
// We do not know whether the label will be executed or not, so `Diverging` must be
// downgraded to `Normal`.
{
match never_loop_block(cx, block, local_labels, main_loop_id) {
NeverLoopResult::Diverging { .. } => NeverLoopResult::Normal,
result => result,
}
},
})),
ExprKind::OffsetOf(_, _)
| ExprKind::Yield(_, _)

View file

@ -183,7 +183,13 @@ fn emit_manual_let_else(
format!("{{ {sn_else} }}")
};
let sn_bl = replace_in_pattern(cx, span, ident_map, pat, &mut app, true);
let sugg = format!("let {sn_bl} = {sn_expr} else {else_bl};");
let sugg = if sn_expr.ends_with('}') {
// let-else statement expressions are not allowed to end with `}`
// https://rust-lang.github.io/rfcs/3137-let-else.html#let-pattern--if--else--else-
format!("let {sn_bl} = ({sn_expr}) else {else_bl};")
} else {
format!("let {sn_bl} = {sn_expr} else {else_bl};")
};
diag.span_suggestion(span, "consider writing", sugg, app);
},
);

View file

@ -1,13 +1,13 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::msrvs::Msrv;
use clippy_utils::res::{MaybeDef, MaybeQPath, MaybeResPath};
use clippy_utils::source::snippet_with_context;
use clippy_utils::{is_none_pattern, msrvs, peel_hir_expr_refs, sym};
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::{Arm, Expr, ExprKind, LangItem, Pat, PatKind, QPath, is_range_literal};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_session::impl_lint_pass;
use rustc_span::{Span, Symbol};
@ -124,8 +124,7 @@ fn check_map(cx: &LateContext<'_>, map: &Expr<'_>, span: Span, msrv: Msrv) {
fn check_as_ref(cx: &LateContext<'_>, expr: &Expr<'_>, span: Span, msrv: Msrv) {
if let ExprKind::MethodCall(seg, callee, [], _) = expr.kind
&& seg.ident.name == sym::as_ref
&& let ty::Adt(adtdef, ..) = cx.typeck_results().expr_ty(callee).kind()
&& cx.tcx.is_diagnostic_item(sym::Option, adtdef.did())
&& cx.typeck_results().expr_ty(callee).is_diag_item(cx, sym::Option)
&& msrv.meets(
cx,
if clippy_utils::is_in_const_context(cx) {
@ -135,19 +134,22 @@ fn check_as_ref(cx: &LateContext<'_>, expr: &Expr<'_>, span: Span, msrv: Msrv) {
},
)
{
if let Some(snippet) = clippy_utils::source::snippet_opt(cx, callee.span) {
span_lint_and_sugg(
cx,
MANUAL_OPTION_AS_SLICE,
span,
"use `Option::as_slice`",
"use",
format!("{snippet}.as_slice()"),
Applicability::MachineApplicable,
);
} else {
span_lint(cx, MANUAL_OPTION_AS_SLICE, span, "use `Option_as_slice`");
}
span_lint_and_then(
cx,
MANUAL_OPTION_AS_SLICE,
span,
"manual implementation of `Option::as_slice`",
|diag| {
let mut app = Applicability::MachineApplicable;
let callee = snippet_with_context(cx, callee.span, expr.span.ctxt(), "_", &mut app).0;
diag.span_suggestion_verbose(
span,
"use `Option::as_slice` directly",
format!("{callee}.as_slice()"),
app,
);
},
);
}
}

View file

@ -214,6 +214,9 @@ fn lint_map_unit_fn(
};
let fn_arg = &map_args.1[0];
#[expect(clippy::items_after_statements, reason = "the const is only used below")]
const SUGG_MSG: &str = "use `if let` instead";
if is_unit_function(cx, fn_arg) {
let mut applicability = Applicability::MachineApplicable;
let msg = suggestion_msg("function", map_type);
@ -226,7 +229,7 @@ fn lint_map_unit_fn(
);
span_lint_and_then(cx, lint, expr.span, msg, |diag| {
diag.span_suggestion(stmt.span, "try", suggestion, applicability);
diag.span_suggestion_verbose(stmt.span, SUGG_MSG, suggestion, applicability);
});
} else if let Some((binding, closure_expr)) = unit_closure(cx, fn_arg) {
let msg = suggestion_msg("closure", map_type);
@ -242,7 +245,7 @@ fn lint_map_unit_fn(
snippet_with_applicability(cx, var_arg.span, "_", &mut applicability),
snippet_with_context(cx, reduced_expr_span, var_arg.span.ctxt(), "_", &mut applicability).0,
);
diag.span_suggestion(stmt.span, "try", suggestion, applicability);
diag.span_suggestion_verbose(stmt.span, SUGG_MSG, suggestion, applicability);
} else {
let suggestion = format!(
"if let {0}({1}) = {2} {{ ... }}",
@ -250,7 +253,7 @@ fn lint_map_unit_fn(
snippet(cx, binding.pat.span, "_"),
snippet(cx, var_arg.span, "_"),
);
diag.span_suggestion(stmt.span, "try", suggestion, Applicability::HasPlaceholders);
diag.span_suggestion_verbose(stmt.span, SUGG_MSG, suggestion, Applicability::HasPlaceholders);
}
});
}

View file

@ -1,5 +1,5 @@
use clippy_utils::consts::ConstEvalCtxt;
use clippy_utils::res::{MaybeDef, MaybeQPath};
use clippy_utils::res::{MaybeDef, MaybeQPath, MaybeResPath};
use clippy_utils::source::{SpanRangeExt as _, indent_of, reindent_multiline};
use rustc_ast::{BindingMode, ByRef};
use rustc_errors::Applicability;
@ -11,7 +11,8 @@ use rustc_span::sym;
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::{expr_type_is_certain, implements_trait};
use clippy_utils::ty::{expr_type_is_certain, implements_trait, is_copy};
use clippy_utils::usage::local_used_after_expr;
use clippy_utils::{is_default_equivalent, is_lint_allowed, peel_blocks, span_contains_comment};
use super::{MANUAL_UNWRAP_OR, MANUAL_UNWRAP_OR_DEFAULT};
@ -87,7 +88,9 @@ fn handle(
binding_id: HirId,
) {
// Only deal with situations where both alternatives return the same non-adjusted type.
if cx.typeck_results().expr_ty(body_some) != cx.typeck_results().expr_ty(body_none) {
if cx.typeck_results().expr_ty(body_some) != cx.typeck_results().expr_ty(body_none)
|| !safe_to_move_scrutinee(cx, expr, condition)
{
return;
}
@ -185,6 +188,29 @@ fn find_type_name<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<&'static
}
}
/// Checks whether it is safe to move scrutinee.
/// It is not safe to move if:
/// 1. `scrutinee` is a `Result` that doesn't implemenet `Copy`, mainly because the `Err`
/// variant is not copyable.
/// 2. `expr` is a local variable that is used after the if-let-else expression.
/// ```rust,ignore
/// let foo: Result<usize, String> = Ok(0);
/// let v = if let Ok(v) = foo { v } else { 1 };
/// let bar = foo;
/// ```
fn safe_to_move_scrutinee(cx: &LateContext<'_>, expr: &Expr<'_>, scrutinee: &Expr<'_>) -> bool {
if let Some(hir_id) = scrutinee.res_local_id()
&& let scrutinee_ty = cx.typeck_results().expr_ty(scrutinee)
&& scrutinee_ty.is_diag_item(cx, sym::Result)
&& !is_copy(cx, scrutinee_ty)
&& local_used_after_expr(cx, hir_id, expr)
{
false
} else {
true
}
}
pub fn check_match<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,

View file

@ -1,6 +1,7 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::res::{MaybeDef, MaybeQPath};
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::option_arg_ty;
use clippy_utils::{is_none_arm, peel_blocks};
use rustc_errors::Applicability;
use rustc_hir::{Arm, BindingMode, ByRef, Expr, ExprKind, LangItem, Mutability, PatKind, QPath};
@ -10,54 +11,77 @@ use rustc_middle::ty;
use super::MATCH_AS_REF;
pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) {
if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() {
let arm_ref_mut = if is_none_arm(cx, &arms[0]) {
is_ref_some_arm(cx, &arms[1])
} else if is_none_arm(cx, &arms[1]) {
is_ref_some_arm(cx, &arms[0])
if let [arm1, arm2] = arms
&& arm1.guard.is_none()
&& arm2.guard.is_none()
&& let Some(arm_ref_mutbl) = if is_none_arm(cx, arm1) {
as_ref_some_arm(cx, arm2)
} else if is_none_arm(cx, arm2) {
as_ref_some_arm(cx, arm1)
} else {
None
};
if let Some(rb) = arm_ref_mut {
let suggestion = match rb {
Mutability::Not => "as_ref",
Mutability::Mut => "as_mut",
};
let output_ty = cx.typeck_results().expr_ty(expr);
let input_ty = cx.typeck_results().expr_ty(ex);
let cast = if let ty::Adt(_, args) = input_ty.kind()
&& let input_ty = args.type_at(0)
&& let ty::Adt(_, args) = output_ty.kind()
&& let output_ty = args.type_at(0)
&& let ty::Ref(_, output_ty, _) = *output_ty.kind()
&& input_ty != output_ty
{
".map(|x| x as _)"
} else {
""
};
let mut applicability = Applicability::MachineApplicable;
span_lint_and_sugg(
cx,
MATCH_AS_REF,
expr.span,
format!("use `{suggestion}()` instead"),
"try",
format!(
"{}.{suggestion}(){cast}",
snippet_with_applicability(cx, ex.span, "_", &mut applicability),
),
applicability,
);
}
&& let output_ty = cx.typeck_results().expr_ty(expr)
&& let input_ty = cx.typeck_results().expr_ty(ex)
&& let Some(input_ty) = option_arg_ty(cx, input_ty)
&& let Some(output_ty) = option_arg_ty(cx, output_ty)
&& let ty::Ref(_, output_ty, output_mutbl) = *output_ty.kind()
{
let method = match arm_ref_mutbl {
Mutability::Not => "as_ref",
Mutability::Mut => "as_mut",
};
// ```
// let _: Option<&T> = match opt {
// Some(ref mut t) => Some(t),
// None => None,
// };
// ```
// We need to suggest `t.as_ref()` in order downcast the reference from `&mut` to `&`.
// We may or may not need to cast the type as well, for which we'd need `.map()`, and that could
// theoretically take care of the reference downcasting as well, but we chose to keep these two
// operations separate
let need_as_ref = arm_ref_mutbl == Mutability::Mut && output_mutbl == Mutability::Not;
let cast = if input_ty == output_ty { "" } else { ".map(|x| x as _)" };
let mut applicability = Applicability::MachineApplicable;
span_lint_and_then(
cx,
MATCH_AS_REF,
expr.span,
format!("manual implementation of `Option::{method}`"),
|diag| {
if need_as_ref {
diag.note("but the type is coerced to a non-mutable reference, and so `as_ref` can used instead");
diag.span_suggestion_verbose(
expr.span,
"use `Option::as_ref()`",
format!(
"{}.as_ref(){cast}",
Sugg::hir_with_applicability(cx, ex, "_", &mut applicability).maybe_paren(),
),
applicability,
);
} else {
diag.span_suggestion_verbose(
expr.span,
format!("use `Option::{method}()` directly"),
format!(
"{}.{method}(){cast}",
Sugg::hir_with_applicability(cx, ex, "_", &mut applicability).maybe_paren(),
),
applicability,
);
}
},
);
}
}
// Checks if arm has the form `Some(ref v) => Some(v)` (checks for `ref` and `ref mut`)
fn is_ref_some_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> Option<Mutability> {
fn as_ref_some_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> Option<Mutability> {
if let PatKind::TupleStruct(ref qpath, [first_pat, ..], _) = arm.pat.kind
&& cx
.qpath_res(qpath, arm.pat.hir_id)

View file

@ -108,7 +108,7 @@ pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) {
},
_,
) => path_prefix.with_prefix(path.segments),
_ => (),
QPath::TypeRelative(..) => (),
}
});
}

View file

@ -416,7 +416,7 @@ fn get_ident(path: &QPath<'_>) -> Option<rustc_span::symbol::Ident> {
let name = path.segments[0].ident;
Some(name)
},
_ => None,
QPath::TypeRelative(..) => None,
}
}

View file

@ -0,0 +1,85 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::res::{MaybeDef, MaybeResPath, MaybeTypeckRes};
use clippy_utils::sym;
use rustc_errors::Applicability;
use rustc_hir::{Body, Closure, Expr, ExprKind};
use rustc_lint::LateContext;
use rustc_span::Span;
use super::LINES_FILTER_MAP_OK;
pub(super) fn check_flatten(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, call_span: Span, msrv: Msrv) {
if cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::Iterator)
&& cx
.typeck_results()
.expr_ty_adjusted(recv)
.is_diag_item(cx, sym::IoLines)
&& msrv.meets(cx, msrvs::MAP_WHILE)
{
emit(cx, recv, "flatten", call_span);
}
}
pub(super) fn check_filter_or_flat_map(
cx: &LateContext<'_>,
expr: &Expr<'_>,
recv: &Expr<'_>,
method_name: &'static str,
method_arg: &Expr<'_>,
call_span: Span,
msrv: Msrv,
) {
if cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::Iterator)
&& cx
.typeck_results()
.expr_ty_adjusted(recv)
.is_diag_item(cx, sym::IoLines)
&& match method_arg.kind {
// Detect `Result::ok`
ExprKind::Path(ref qpath) => cx
.qpath_res(qpath, method_arg.hir_id)
.is_diag_item(cx, sym::result_ok_method),
// 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
{
method.ident.name == sym::ok
&& receiver.res_local_id() == Some(param.pat.hir_id)
&& cx.ty_based_def(*value).is_diag_item(cx, sym::result_ok_method)
} else {
false
}
},
_ => false,
}
&& msrv.meets(cx, msrvs::MAP_WHILE)
{
emit(cx, recv, method_name, call_span);
}
}
fn emit(cx: &LateContext<'_>, recv: &Expr<'_>, method_name: &'static str, call_span: Span) {
span_lint_and_then(
cx,
LINES_FILTER_MAP_OK,
call_span,
format!("`{method_name}()` will run forever if the iterator repeatedly produces an `Err`"),
|diag| {
diag.span_note(
recv.span,
"this expression returning a `std::io::Lines` may produce \
an infinite number of `Err` in case of a read error",
);
diag.span_suggestion(
call_span,
"replace with",
"map_while(Result::ok)",
Applicability::MaybeIncorrect,
);
},
);
}

View file

@ -56,6 +56,7 @@ mod iter_with_drain;
mod iterator_step_by_zero;
mod join_absolute_paths;
mod lib;
mod lines_filter_map_ok;
mod manual_c_str_literals;
mod manual_contains;
mod manual_inspect;
@ -4665,6 +4666,55 @@ declare_clippy_lint! {
"making no use of the \"map closure\" when calling `.map_or_else(|| 2 * k, |n| n)`"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for usage 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 are 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
/// ```no_run
/// # 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:
/// ```no_run
/// # 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()`, `flat_map()`, or `flatten()` might cause an infinite loop"
}
#[expect(clippy::struct_excessive_bools)]
pub struct Methods {
avoid_breaking_exported_api: bool,
@ -4847,6 +4897,7 @@ impl_lint_pass!(Methods => [
IP_CONSTANT,
REDUNDANT_ITER_CLONED,
UNNECESSARY_OPTION_MAP_OR_ELSE,
LINES_FILTER_MAP_OK,
]);
/// Extracts a method call name, args, and `Span` of the method name.
@ -5050,7 +5101,11 @@ impl Methods {
(sym::bytes, []) => unbuffered_bytes::check(cx, expr, recv),
(sym::cloned, []) => {
cloned_instead_of_copied::check(cx, expr, recv, span, self.msrv);
option_as_ref_cloned::check(cx, recv, span);
if let Some((method @ (sym::as_ref | sym::as_mut), as_ref_recv, [], as_ref_ident_span, _)) =
method_call(recv)
{
option_as_ref_cloned::check(cx, span, method, as_ref_recv, as_ref_ident_span);
}
},
(sym::collect, []) if cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::Iterator) => {
needless_collect::check(cx, span, expr, recv, call_span);
@ -5162,32 +5217,47 @@ impl Methods {
},
(sym::filter_map, [arg]) => {
unused_enumerate_index::check(cx, expr, recv, arg);
unnecessary_filter_map::check(cx, expr, arg, name);
unnecessary_filter_map::check(cx, expr, arg, call_span, unnecessary_filter_map::Kind::FilterMap);
filter_map_bool_then::check(cx, expr, arg, call_span);
filter_map_identity::check(cx, expr, arg, span);
lines_filter_map_ok::check_filter_or_flat_map(
cx,
expr,
recv,
"filter_map",
arg,
call_span,
self.msrv,
);
},
(sym::find_map, [arg]) => {
unused_enumerate_index::check(cx, expr, recv, arg);
unnecessary_filter_map::check(cx, expr, arg, name);
unnecessary_filter_map::check(cx, expr, arg, call_span, unnecessary_filter_map::Kind::FindMap);
},
(sym::flat_map, [arg]) => {
unused_enumerate_index::check(cx, expr, recv, arg);
flat_map_identity::check(cx, expr, arg, span);
flat_map_option::check(cx, expr, arg, span);
lines_filter_map_ok::check_filter_or_flat_map(
cx, expr, recv, "flat_map", arg, call_span, self.msrv,
);
},
(sym::flatten, []) => match method_call(recv) {
Some((sym::map, recv, [map_arg], map_span, _)) => {
map_flatten::check(cx, expr, recv, map_arg, map_span);
},
Some((sym::cloned, recv2, [], _, _)) => iter_overeager_cloned::check(
cx,
expr,
recv,
recv2,
iter_overeager_cloned::Op::LaterCloned,
true,
),
_ => {},
(sym::flatten, []) => {
match method_call(recv) {
Some((sym::map, recv, [map_arg], map_span, _)) => {
map_flatten::check(cx, expr, recv, map_arg, map_span);
},
Some((sym::cloned, recv2, [], _, _)) => iter_overeager_cloned::check(
cx,
expr,
recv,
recv2,
iter_overeager_cloned::Op::LaterCloned,
true,
),
_ => {},
}
lines_filter_map_ok::check_flatten(cx, expr, recv, call_span, self.msrv);
},
(sym::fold, [init, acc]) => {
manual_try_fold::check(cx, expr, init, acc, call_span, self.msrv);

View file

@ -4,24 +4,28 @@ use clippy_utils::sym;
use rustc_errors::Applicability;
use rustc_hir::Expr;
use rustc_lint::LateContext;
use rustc_span::Span;
use rustc_span::{Span, Symbol};
use super::{OPTION_AS_REF_CLONED, method_call};
use super::OPTION_AS_REF_CLONED;
pub(super) fn check(cx: &LateContext<'_>, cloned_recv: &Expr<'_>, cloned_ident_span: Span) {
if let Some((method @ (sym::as_ref | sym::as_mut), as_ref_recv, [], as_ref_ident_span, _)) =
method_call(cloned_recv)
&& cx
.typeck_results()
.expr_ty(as_ref_recv)
.peel_refs()
.is_diag_item(cx, sym::Option)
pub(super) fn check(
cx: &LateContext<'_>,
cloned_ident_span: Span,
as_ref_method: Symbol,
as_ref_recv: &Expr<'_>,
as_ref_ident_span: Span,
) {
if cx
.typeck_results()
.expr_ty(as_ref_recv)
.peel_refs()
.is_diag_item(cx, sym::Option)
{
span_lint_and_sugg(
cx,
OPTION_AS_REF_CLONED,
as_ref_ident_span.to(cloned_ident_span),
format!("cloning an `Option<_>` using `.{method}().cloned()`"),
format!("cloning an `Option<_>` using `.{as_ref_method}().cloned()`"),
"this can be written more concisely by cloning the `Option<_>` directly",
"clone".into(),
Applicability::MachineApplicable,

View file

@ -1,4 +1,4 @@
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::res::{MaybeDef, MaybeTypeckRes};
use clippy_utils::source::{snippet, snippet_with_applicability};
use clippy_utils::sugg::deref_closure_args;
@ -34,79 +34,67 @@ pub(super) fn check<'tcx>(
{
let msg = format!("called `{option_check_method}()` after searching an `Iterator` with `{search_method}`");
let search_snippet = snippet(cx, search_arg.span, "..");
if search_snippet.lines().count() <= 1 {
// suggest `any(|x| ..)` instead of `any(|&x| ..)` for `find(|&x| ..).is_some()`
// suggest `any(|..| *..)` instead of `any(|..| **..)` for `find(|..| **..).is_some()`
let mut applicability = Applicability::MachineApplicable;
let any_search_snippet = if search_method == sym::find
&& let ExprKind::Closure(&hir::Closure { body, .. }) = search_arg.kind
&& let closure_body = cx.tcx.hir_body(body)
&& let Some(closure_arg) = closure_body.params.first()
{
if let PatKind::Ref(..) = closure_arg.pat.kind {
Some(search_snippet.replacen('&', "", 1))
} else if let PatKind::Binding(..) = strip_pat_refs(closure_arg.pat).kind {
// `find()` provides a reference to the item, but `any` does not,
// so we should fix item usages for suggestion
if let Some(closure_sugg) = deref_closure_args(cx, search_arg) {
applicability = closure_sugg.applicability;
Some(closure_sugg.suggestion)
} else {
Some(search_snippet.to_string())
}
// suggest `any(|x| ..)` instead of `any(|&x| ..)` for `find(|&x| ..).is_some()`
// suggest `any(|..| *..)` instead of `any(|..| **..)` for `find(|..| **..).is_some()`
let mut applicability = Applicability::MachineApplicable;
let any_search_snippet = if search_method == sym::find
&& let ExprKind::Closure(&hir::Closure { body, .. }) = search_arg.kind
&& let closure_body = cx.tcx.hir_body(body)
&& let Some(closure_arg) = closure_body.params.first()
{
if let PatKind::Ref(..) = closure_arg.pat.kind {
Some(search_snippet.replacen('&', "", 1))
} else if let PatKind::Binding(..) = strip_pat_refs(closure_arg.pat).kind {
// `find()` provides a reference to the item, but `any` does not,
// so we should fix item usages for suggestion
if let Some(closure_sugg) = deref_closure_args(cx, search_arg) {
applicability = closure_sugg.applicability;
Some(closure_sugg.suggestion)
} else {
None
Some(search_snippet.to_string())
}
} else {
None
};
// add note if not multi-line
if is_some {
span_lint_and_sugg(
cx,
SEARCH_IS_SOME,
method_span.with_hi(expr.span.hi()),
msg,
"consider using",
format!(
"any({})",
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
),
applicability,
);
} else {
let iter = snippet(cx, search_recv.span, "..");
let sugg = if is_receiver_of_method_call(cx, expr) {
format!(
"(!{iter}.any({}))",
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
)
} else {
format!(
"!{iter}.any({})",
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
)
};
span_lint_and_sugg(
cx,
SEARCH_IS_SOME,
expr.span,
msg,
"consider using",
sugg,
applicability,
);
}
} else {
let hint = format!(
"this is more succinctly expressed by calling `any()`{}",
if option_check_method == "is_none" {
" with negation"
} else {
""
}
None
};
// add note if not multi-line
if is_some {
span_lint_and_sugg(
cx,
SEARCH_IS_SOME,
method_span.with_hi(expr.span.hi()),
msg,
"consider using",
format!(
"any({})",
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
),
applicability,
);
} else {
let iter = snippet(cx, search_recv.span, "..");
let sugg = if is_receiver_of_method_call(cx, expr) {
format!(
"(!{iter}.any({}))",
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
)
} else {
format!(
"!{iter}.any({})",
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
)
};
span_lint_and_sugg(
cx,
SEARCH_IS_SOME,
expr.span,
msg,
"consider using",
sugg,
applicability,
);
span_lint_and_help(cx, SEARCH_IS_SOME, expr.span, msg, None, hint);
}
}
// lint if `find()` is called by `String` or `&str`

View file

@ -2,15 +2,15 @@ use super::utils::clone_or_copy_needed;
use clippy_utils::diagnostics::span_lint;
use clippy_utils::res::{MaybeDef, MaybeQPath, MaybeResPath, MaybeTypeckRes};
use clippy_utils::sym;
use clippy_utils::ty::is_copy;
use clippy_utils::ty::{is_copy, option_arg_ty};
use clippy_utils::usage::mutated_variables;
use clippy_utils::visitors::{Descend, for_each_expr_without_closures};
use core::ops::ControlFlow;
use rustc_hir as hir;
use rustc_hir::LangItem::{OptionNone, OptionSome};
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::Symbol;
use rustc_span::Span;
use std::fmt::Display;
use super::{UNNECESSARY_FILTER_MAP, UNNECESSARY_FIND_MAP};
@ -18,7 +18,8 @@ pub(super) fn check<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx hir::Expr<'tcx>,
arg: &'tcx hir::Expr<'tcx>,
name: Symbol,
call_span: Span,
kind: Kind,
) {
if !cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::Iterator) {
return;
@ -45,61 +46,88 @@ pub(super) fn check<'tcx>(
let in_ty = cx.typeck_results().node_type(body.params[0].hir_id);
let sugg = if !found_filtering {
// Check if the closure is .filter_map(|x| Some(x))
if name == sym::filter_map
&& let hir::ExprKind::Call(expr, args) = body.value.kind
if kind.is_filter_map()
&& let hir::ExprKind::Call(expr, [arg]) = body.value.kind
&& expr.res(cx).ctor_parent(cx).is_lang_item(cx, OptionSome)
&& let hir::ExprKind::Path(_) = args[0].kind
&& let hir::ExprKind::Path(_) = arg.kind
{
span_lint(
cx,
UNNECESSARY_FILTER_MAP,
expr.span,
call_span,
String::from("this call to `.filter_map(..)` is unnecessary"),
);
return;
}
if name == sym::filter_map {
"map(..)"
} else {
"map(..).next()"
match kind {
Kind::FilterMap => "map(..)",
Kind::FindMap => "map(..).next()",
}
} else if !found_mapping && !mutates_arg && (!clone_or_copy_needed || is_copy(cx, in_ty)) {
match cx.typeck_results().expr_ty(body.value).kind() {
ty::Adt(adt, subst)
if cx.tcx.is_diagnostic_item(sym::Option, adt.did()) && in_ty == subst.type_at(0) =>
{
if name == sym::filter_map {
"filter(..)"
} else {
"find(..)"
}
},
_ => return,
let ty = cx.typeck_results().expr_ty(body.value);
if option_arg_ty(cx, ty).is_some_and(|t| t == in_ty) {
match kind {
Kind::FilterMap => "filter(..)",
Kind::FindMap => "find(..)",
}
} else {
return;
}
} else {
return;
};
span_lint(
cx,
if name == sym::filter_map {
UNNECESSARY_FILTER_MAP
} else {
UNNECESSARY_FIND_MAP
match kind {
Kind::FilterMap => UNNECESSARY_FILTER_MAP,
Kind::FindMap => UNNECESSARY_FIND_MAP,
},
expr.span,
format!("this `.{name}(..)` can be written more simply using `.{sugg}`"),
call_span,
format!("this `.{kind}(..)` can be written more simply using `.{sugg}`"),
);
}
}
#[derive(Clone, Copy)]
pub(super) enum Kind {
FilterMap,
FindMap,
}
impl Kind {
fn is_filter_map(self) -> bool {
matches!(self, Self::FilterMap)
}
}
impl Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FilterMap => f.write_str("filter_map"),
Self::FindMap => f.write_str("find_map"),
}
}
}
// returns (found_mapping, found_filtering)
fn check_expression<'tcx>(cx: &LateContext<'tcx>, arg_id: hir::HirId, expr: &'tcx hir::Expr<'_>) -> (bool, bool) {
match expr.kind {
hir::ExprKind::Path(ref path)
if cx
.qpath_res(path, expr.hir_id)
.ctor_parent(cx)
.is_lang_item(cx, OptionNone) =>
{
// None
(false, true)
},
hir::ExprKind::Call(func, args) => {
if func.res(cx).ctor_parent(cx).is_lang_item(cx, OptionSome) {
if args[0].res_local_id() == Some(arg_id) {
// Some(arg_id)
return (false, false);
}
// Some(not arg_id)
return (true, false);
}
(true, true)
@ -109,8 +137,10 @@ fn check_expression<'tcx>(cx: &LateContext<'tcx>, arg_id: hir::HirId, expr: &'tc
&& cx.typeck_results().expr_ty(recv).is_bool()
&& arg.res_local_id() == Some(arg_id)
{
// bool.then_some(arg_id)
(false, true)
} else {
// bool.then_some(not arg_id)
(true, true)
}
},
@ -134,14 +164,6 @@ fn check_expression<'tcx>(cx: &LateContext<'tcx>, arg_id: hir::HirId, expr: &'tc
let else_check = check_expression(cx, arg_id, else_arm);
(if_check.0 | else_check.0, if_check.1 | else_check.1)
},
hir::ExprKind::Path(ref path)
if cx
.qpath_res(path, expr.hir_id)
.ctor_parent(cx)
.is_lang_item(cx, OptionNone) =>
{
(false, true)
},
_ => (true, true),
}
}

View file

@ -8,7 +8,7 @@ use super::UNNEEDED_WILDCARD_PATTERN;
pub(super) fn check(cx: &EarlyContext<'_>, pat: &Pat) {
if let PatKind::TupleStruct(_, _, ref patterns) | PatKind::Tuple(ref patterns) = pat.kind
&& let Some(rest_index) = patterns.iter().position(|pat| pat.is_rest())
&& let Some(rest_index) = patterns.iter().position(Pat::is_rest)
{
if let Some((left_index, left_pat)) = patterns[..rest_index]
.iter()

View file

@ -147,7 +147,7 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessBorrowsForGenericArgs<'tcx> {
fn path_has_args(p: &QPath<'_>) -> bool {
match *p {
QPath::Resolved(_, Path { segments: [.., s], .. }) | QPath::TypeRelative(_, s) => s.args.is_some(),
_ => false,
QPath::Resolved(..) => false,
}
}

View file

@ -1,7 +1,7 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::higher::If;
use clippy_utils::is_from_proc_macro;
use clippy_utils::source::SpanRangeExt;
use clippy_utils::source::{SpanRangeExt, walk_span_to_context};
use rustc_errors::Applicability;
use rustc_hir::{ExprKind, Stmt, StmtKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
@ -29,13 +29,13 @@ declare_clippy_lint! {
/// really_expensive_condition_with_side_effects(&mut i);
/// ```
#[clippy::version = "1.72.0"]
pub NEEDLESS_IF,
pub NEEDLESS_IFS,
complexity,
"checks for empty if branches"
}
declare_lint_pass!(NeedlessIf => [NEEDLESS_IF]);
declare_lint_pass!(NeedlessIfs => [NEEDLESS_IFS]);
impl LateLintPass<'_> for NeedlessIf {
impl LateLintPass<'_> for NeedlessIfs {
fn check_stmt<'tcx>(&mut self, cx: &LateContext<'tcx>, stmt: &Stmt<'tcx>) {
if let StmtKind::Expr(expr) = stmt.kind
&& let Some(If {
@ -56,12 +56,13 @@ impl LateLintPass<'_> for NeedlessIf {
src.bytes()
.all(|ch| matches!(ch, b'{' | b'}') || ch.is_ascii_whitespace())
})
&& let Some(cond_snippet) = cond.span.get_source_text(cx)
&& let Some(cond_span) = walk_span_to_context(cond.span, expr.span.ctxt())
&& let Some(cond_snippet) = cond_span.get_source_text(cx)
&& !is_from_proc_macro(cx, expr)
{
span_lint_and_sugg(
cx,
NEEDLESS_IF,
NEEDLESS_IFS,
stmt.span,
"this `if` branch is empty",
"you can remove it",

View file

@ -6,7 +6,9 @@ use rustc_data_structures::fx::FxHashMap;
use rustc_errors::Applicability;
use rustc_hir::def_id::DefId;
use rustc_hir::hir_id::HirIdMap;
use rustc_hir::{Body, Expr, ExprKind, HirId, ImplItem, ImplItemImplKind, ImplItemKind, Node, PatKind, TraitItem, TraitItemKind};
use rustc_hir::{
Body, Expr, ExprKind, HirId, ImplItem, ImplItemImplKind, ImplItemKind, Node, PatKind, TraitItem, TraitItemKind,
};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, ConstKind, GenericArgKind, GenericArgsRef};
use rustc_session::impl_lint_pass;

View file

@ -8,46 +8,52 @@ use rustc_span::Span;
use super::DOUBLE_COMPARISONS;
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, op: BinOpKind, lhs: &'tcx Expr<'_>, rhs: &'tcx Expr<'_>, span: Span) {
let (lkind, llhs, lrhs, rkind, rlhs, rrhs) = match (&lhs.kind, &rhs.kind) {
(ExprKind::Binary(lb, llhs, lrhs), ExprKind::Binary(rb, rlhs, rrhs)) => {
(lb.node, llhs, lrhs, rb.node, rlhs, rrhs)
},
_ => return,
};
if !(eq_expr_value(cx, llhs, rlhs) && eq_expr_value(cx, lrhs, rrhs)) {
return;
}
macro_rules! lint_double_comparison {
($op:tt) => {{
let mut applicability = Applicability::MachineApplicable;
let lhs_str = snippet_with_applicability(cx, llhs.span, "", &mut applicability);
let rhs_str = snippet_with_applicability(cx, lrhs.span, "", &mut applicability);
let sugg = format!("{} {} {}", lhs_str, stringify!($op), rhs_str);
span_lint_and_sugg(
cx,
DOUBLE_COMPARISONS,
span,
"this binary expression can be simplified",
"try",
sugg,
applicability,
);
}};
}
match (op, lkind, rkind) {
(BinOpKind::Or, BinOpKind::Eq, BinOpKind::Lt) | (BinOpKind::Or, BinOpKind::Lt, BinOpKind::Eq) => {
lint_double_comparison!(<=);
},
(BinOpKind::Or, BinOpKind::Eq, BinOpKind::Gt) | (BinOpKind::Or, BinOpKind::Gt, BinOpKind::Eq) => {
lint_double_comparison!(>=);
},
(BinOpKind::Or, BinOpKind::Lt, BinOpKind::Gt) | (BinOpKind::Or, BinOpKind::Gt, BinOpKind::Lt) => {
lint_double_comparison!(!=);
},
(BinOpKind::And, BinOpKind::Le, BinOpKind::Ge) | (BinOpKind::And, BinOpKind::Ge, BinOpKind::Le) => {
lint_double_comparison!(==);
},
_ => (),
pub(super) fn check(cx: &LateContext<'_>, op: BinOpKind, lhs: &Expr<'_>, rhs: &Expr<'_>, span: Span) {
if let ExprKind::Binary(lop, llhs, lrhs) = lhs.kind
&& let ExprKind::Binary(rop, rlhs, rrhs) = rhs.kind
&& eq_expr_value(cx, llhs, rlhs)
&& eq_expr_value(cx, lrhs, rrhs)
{
let op = match (op, lop.node, rop.node) {
// x == y || x < y => x <= y
(BinOpKind::Or, BinOpKind::Eq, BinOpKind::Lt)
// x < y || x == y => x <= y
| (BinOpKind::Or, BinOpKind::Lt, BinOpKind::Eq) => {
"<="
},
// x == y || x > y => x >= y
(BinOpKind::Or, BinOpKind::Eq, BinOpKind::Gt)
// x > y || x == y => x >= y
| (BinOpKind::Or, BinOpKind::Gt, BinOpKind::Eq) => {
">="
},
// x < y || x > y => x != y
(BinOpKind::Or, BinOpKind::Lt, BinOpKind::Gt)
// x > y || x < y => x != y
| (BinOpKind::Or, BinOpKind::Gt, BinOpKind::Lt) => {
"!="
},
// x <= y && x >= y => x == y
(BinOpKind::And, BinOpKind::Le, BinOpKind::Ge)
// x >= y && x <= y => x == y
| (BinOpKind::And, BinOpKind::Ge, BinOpKind::Le) => {
"=="
},
_ => return,
};
let mut applicability = Applicability::MachineApplicable;
let lhs_str = snippet_with_applicability(cx, llhs.span, "", &mut applicability);
let rhs_str = snippet_with_applicability(cx, lrhs.span, "", &mut applicability);
let sugg = format!("{lhs_str} {op} {rhs_str}");
span_lint_and_sugg(
cx,
DOUBLE_COMPARISONS,
span,
"this binary expression can be simplified",
"try",
sugg,
applicability,
);
}
}

View file

@ -0,0 +1,24 @@
use clippy_utils::diagnostics::span_lint;
use rustc_ast::BinOpKind;
use rustc_hir::Expr;
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::Span;
use super::INTEGER_DIVISION_REMAINDER_USED;
pub(super) fn check(cx: &LateContext<'_>, op: BinOpKind, lhs: &Expr<'_>, rhs: &Expr<'_>, span: Span) {
if let BinOpKind::Div | BinOpKind::Rem = op
&& let lhs_ty = cx.typeck_results().expr_ty(lhs)
&& let rhs_ty = cx.typeck_results().expr_ty(rhs)
&& let ty::Int(_) | ty::Uint(_) = lhs_ty.peel_refs().kind()
&& let ty::Int(_) | ty::Uint(_) = rhs_ty.peel_refs().kind()
{
span_lint(
cx,
INTEGER_DIVISION_REMAINDER_USED,
span.source_callsite(),
format!("use of `{}` has been disallowed in this context", op.as_str()),
);
}
}

View file

@ -1,9 +1,8 @@
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_hir::{BinOpKind, Expr, ExprKind};
use rustc_lint::LateContext;
use rustc_middle::ty::layout::LayoutOf;
use rustc_middle::ty::{self, IntTy, UintTy};
use rustc_session::declare_lint_pass;
use rustc_span::Span;
use clippy_utils::comparisons;
@ -12,29 +11,26 @@ use clippy_utils::consts::{ConstEvalCtxt, FullInt};
use clippy_utils::diagnostics::span_lint;
use clippy_utils::source::snippet_with_context;
declare_clippy_lint! {
/// ### What it does
/// Checks for comparisons where the relation is always either
/// true or false, but where one side has been upcast so that the comparison is
/// necessary. Only integer types are checked.
///
/// ### Why is this bad?
/// An expression like `let x : u8 = ...; (x as u32) > 300`
/// will mistakenly imply that it is possible for `x` to be outside the range of
/// `u8`.
///
/// ### Example
/// ```no_run
/// let x: u8 = 1;
/// (x as u32) > 300;
/// ```
#[clippy::version = "pre 1.29.0"]
pub INVALID_UPCAST_COMPARISONS,
pedantic,
"a comparison involving an upcast which is always true or false"
}
use super::INVALID_UPCAST_COMPARISONS;
declare_lint_pass!(InvalidUpcastComparisons => [INVALID_UPCAST_COMPARISONS]);
pub(super) fn check<'tcx>(
cx: &LateContext<'tcx>,
cmp: BinOpKind,
lhs: &'tcx Expr<'_>,
rhs: &'tcx Expr<'_>,
span: Span,
) {
let normalized = comparisons::normalize_comparison(cmp, lhs, rhs);
let Some((rel, normalized_lhs, normalized_rhs)) = normalized else {
return;
};
let lhs_bounds = numeric_cast_precast_bounds(cx, normalized_lhs);
let rhs_bounds = numeric_cast_precast_bounds(cx, normalized_rhs);
upcast_comparison_bounds_err(cx, span, rel, lhs_bounds, normalized_lhs, normalized_rhs, false);
upcast_comparison_bounds_err(cx, span, rel, rhs_bounds, normalized_rhs, normalized_lhs, true);
}
fn numeric_cast_precast_bounds(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<(FullInt, FullInt)> {
if let ExprKind::Cast(cast_exp, _) = expr.kind {
@ -68,6 +64,47 @@ fn numeric_cast_precast_bounds(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<
}
}
fn upcast_comparison_bounds_err<'tcx>(
cx: &LateContext<'tcx>,
span: Span,
rel: Rel,
lhs_bounds: Option<(FullInt, FullInt)>,
lhs: &'tcx Expr<'_>,
rhs: &'tcx Expr<'_>,
invert: bool,
) {
if let Some((lb, ub)) = lhs_bounds
&& let Some(norm_rhs_val) = ConstEvalCtxt::new(cx).eval_full_int(rhs, span.ctxt())
{
match rel {
Rel::Eq => {
if norm_rhs_val < lb || ub < norm_rhs_val {
err_upcast_comparison(cx, span, lhs, false);
}
},
Rel::Ne => {
if norm_rhs_val < lb || ub < norm_rhs_val {
err_upcast_comparison(cx, span, lhs, true);
}
},
Rel::Lt => {
if (invert && norm_rhs_val < lb) || (!invert && ub < norm_rhs_val) {
err_upcast_comparison(cx, span, lhs, true);
} else if (!invert && norm_rhs_val <= lb) || (invert && ub <= norm_rhs_val) {
err_upcast_comparison(cx, span, lhs, false);
}
},
Rel::Le => {
if (invert && norm_rhs_val <= lb) || (!invert && ub <= norm_rhs_val) {
err_upcast_comparison(cx, span, lhs, true);
} else if (!invert && norm_rhs_val < lb) || (invert && ub < norm_rhs_val) {
err_upcast_comparison(cx, span, lhs, false);
}
},
}
}
}
fn err_upcast_comparison(cx: &LateContext<'_>, span: Span, expr: &Expr<'_>, always: bool) {
if let ExprKind::Cast(cast_val, _) = expr.kind {
let mut applicability = Applicability::MachineApplicable;
@ -90,76 +127,3 @@ fn err_upcast_comparison(cx: &LateContext<'_>, span: Span, expr: &Expr<'_>, alwa
);
}
}
fn upcast_comparison_bounds_err<'tcx>(
cx: &LateContext<'tcx>,
span: Span,
rel: Rel,
lhs_bounds: Option<(FullInt, FullInt)>,
lhs: &'tcx Expr<'_>,
rhs: &'tcx Expr<'_>,
invert: bool,
) {
if let Some((lb, ub)) = lhs_bounds
&& let Some(norm_rhs_val) = ConstEvalCtxt::new(cx).eval_full_int(rhs, span.ctxt())
{
if rel == Rel::Eq || rel == Rel::Ne {
if norm_rhs_val < lb || norm_rhs_val > ub {
err_upcast_comparison(cx, span, lhs, rel == Rel::Ne);
}
} else if match rel {
Rel::Lt => {
if invert {
norm_rhs_val < lb
} else {
ub < norm_rhs_val
}
},
Rel::Le => {
if invert {
norm_rhs_val <= lb
} else {
ub <= norm_rhs_val
}
},
Rel::Eq | Rel::Ne => unreachable!(),
} {
err_upcast_comparison(cx, span, lhs, true);
} else if match rel {
Rel::Lt => {
if invert {
norm_rhs_val >= ub
} else {
lb >= norm_rhs_val
}
},
Rel::Le => {
if invert {
norm_rhs_val > ub
} else {
lb > norm_rhs_val
}
},
Rel::Eq | Rel::Ne => unreachable!(),
} {
err_upcast_comparison(cx, span, lhs, false);
}
}
}
impl<'tcx> LateLintPass<'tcx> for InvalidUpcastComparisons {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if let ExprKind::Binary(ref cmp, lhs, rhs) = expr.kind {
let normalized = comparisons::normalize_comparison(cmp.node, lhs, rhs);
let Some((rel, normalized_lhs, normalized_rhs)) = normalized else {
return;
};
let lhs_bounds = numeric_cast_precast_bounds(cx, normalized_lhs);
let rhs_bounds = numeric_cast_precast_bounds(cx, normalized_rhs);
upcast_comparison_bounds_err(cx, expr.span, rel, lhs_bounds, normalized_lhs, normalized_rhs, false);
upcast_comparison_bounds_err(cx, expr.span, rel, rhs_bounds, normalized_rhs, normalized_lhs, true);
}
}
}

View file

@ -1,4 +1,3 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::sugg::{Sugg, has_enclosing_paren};
@ -7,111 +6,69 @@ use rustc_ast::{BinOpKind, LitIntType, LitKind, UnOp};
use rustc_data_structures::packed::Pu128;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_lint::LateContext;
use rustc_middle::ty::{self};
use rustc_session::impl_lint_pass;
use rustc_span::source_map::Spanned;
declare_clippy_lint! {
/// ### What it does
/// Checks for an expression like `(x + (y - 1)) / y` which is a common manual reimplementation
/// of `x.div_ceil(y)`.
///
/// ### Why is this bad?
/// It's simpler, clearer and more readable.
///
/// ### Example
/// ```no_run
/// let x: i32 = 7;
/// let y: i32 = 4;
/// let div = (x + (y - 1)) / y;
/// ```
/// Use instead:
/// ```no_run
/// #![feature(int_roundings)]
/// let x: i32 = 7;
/// let y: i32 = 4;
/// let div = x.div_ceil(y);
/// ```
#[clippy::version = "1.83.0"]
pub MANUAL_DIV_CEIL,
complexity,
"manually reimplementing `div_ceil`"
}
use super::MANUAL_DIV_CEIL;
pub struct ManualDivCeil {
msrv: Msrv,
}
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, op: BinOpKind, lhs: &Expr<'_>, rhs: &Expr<'_>, msrv: Msrv) {
let mut applicability = Applicability::MachineApplicable;
impl ManualDivCeil {
#[must_use]
pub fn new(conf: &'static Conf) -> Self {
Self { msrv: conf.msrv }
}
}
impl_lint_pass!(ManualDivCeil => [MANUAL_DIV_CEIL]);
impl<'tcx> LateLintPass<'tcx> for ManualDivCeil {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
let mut applicability = Applicability::MachineApplicable;
if let ExprKind::Binary(div_op, div_lhs, div_rhs) = expr.kind
&& div_op.node == BinOpKind::Div
&& check_int_ty_and_feature(cx, div_lhs)
&& check_int_ty_and_feature(cx, div_rhs)
&& let ExprKind::Binary(inner_op, inner_lhs, inner_rhs) = div_lhs.kind
&& self.msrv.meets(cx, msrvs::DIV_CEIL)
if op == BinOpKind::Div
&& check_int_ty_and_feature(cx, lhs)
&& check_int_ty_and_feature(cx, rhs)
&& let ExprKind::Binary(inner_op, inner_lhs, inner_rhs) = lhs.kind
&& msrv.meets(cx, msrvs::DIV_CEIL)
{
// (x + (y - 1)) / y
if let ExprKind::Binary(sub_op, sub_lhs, sub_rhs) = inner_rhs.kind
&& inner_op.node == BinOpKind::Add
&& sub_op.node == BinOpKind::Sub
&& check_literal(sub_rhs)
&& check_eq_expr(cx, sub_lhs, rhs)
{
// (x + (y - 1)) / y
if let ExprKind::Binary(sub_op, sub_lhs, sub_rhs) = inner_rhs.kind
&& inner_op.node == BinOpKind::Add
&& sub_op.node == BinOpKind::Sub
&& check_literal(sub_rhs)
&& check_eq_expr(cx, sub_lhs, div_rhs)
{
build_suggestion(cx, expr, inner_lhs, div_rhs, &mut applicability);
return;
}
build_suggestion(cx, expr, inner_lhs, rhs, &mut applicability);
return;
}
// ((y - 1) + x) / y
if let ExprKind::Binary(sub_op, sub_lhs, sub_rhs) = inner_lhs.kind
&& inner_op.node == BinOpKind::Add
&& sub_op.node == BinOpKind::Sub
&& check_literal(sub_rhs)
&& check_eq_expr(cx, sub_lhs, div_rhs)
{
build_suggestion(cx, expr, inner_rhs, div_rhs, &mut applicability);
return;
}
// ((y - 1) + x) / y
if let ExprKind::Binary(sub_op, sub_lhs, sub_rhs) = inner_lhs.kind
&& inner_op.node == BinOpKind::Add
&& sub_op.node == BinOpKind::Sub
&& check_literal(sub_rhs)
&& check_eq_expr(cx, sub_lhs, rhs)
{
build_suggestion(cx, expr, inner_rhs, rhs, &mut applicability);
return;
}
// (x + y - 1) / y
if let ExprKind::Binary(add_op, add_lhs, add_rhs) = inner_lhs.kind
&& inner_op.node == BinOpKind::Sub
&& add_op.node == BinOpKind::Add
&& check_literal(inner_rhs)
&& check_eq_expr(cx, add_rhs, div_rhs)
{
build_suggestion(cx, expr, add_lhs, div_rhs, &mut applicability);
}
// (x + y - 1) / y
if let ExprKind::Binary(add_op, add_lhs, add_rhs) = inner_lhs.kind
&& inner_op.node == BinOpKind::Sub
&& add_op.node == BinOpKind::Add
&& check_literal(inner_rhs)
&& check_eq_expr(cx, add_rhs, rhs)
{
build_suggestion(cx, expr, add_lhs, rhs, &mut applicability);
}
// (x + (Y - 1)) / Y
if inner_op.node == BinOpKind::Add && differ_by_one(inner_rhs, div_rhs) {
build_suggestion(cx, expr, inner_lhs, div_rhs, &mut applicability);
}
// (x + (Y - 1)) / Y
if inner_op.node == BinOpKind::Add && differ_by_one(inner_rhs, rhs) {
build_suggestion(cx, expr, inner_lhs, rhs, &mut applicability);
}
// ((Y - 1) + x) / Y
if inner_op.node == BinOpKind::Add && differ_by_one(inner_lhs, div_rhs) {
build_suggestion(cx, expr, inner_rhs, div_rhs, &mut applicability);
}
// ((Y - 1) + x) / Y
if inner_op.node == BinOpKind::Add && differ_by_one(inner_lhs, rhs) {
build_suggestion(cx, expr, inner_rhs, rhs, &mut applicability);
}
// (x - (-Y - 1)) / Y
if inner_op.node == BinOpKind::Sub
&& let ExprKind::Unary(UnOp::Neg, abs_div_rhs) = div_rhs.kind
&& differ_by_one(abs_div_rhs, inner_rhs)
{
build_suggestion(cx, expr, inner_lhs, div_rhs, &mut applicability);
}
// (x - (-Y - 1)) / Y
if inner_op.node == BinOpKind::Sub
&& let ExprKind::Unary(UnOp::Neg, abs_div_rhs) = rhs.kind
&& differ_by_one(abs_div_rhs, inner_rhs)
{
build_suggestion(cx, expr, inner_lhs, rhs, &mut applicability);
}
}
}

View file

@ -11,6 +11,9 @@ mod float_cmp;
mod float_equality_without_abs;
mod identity_op;
mod integer_division;
mod integer_division_remainder_used;
mod invalid_upcast_comparisons;
mod manual_div_ceil;
mod manual_is_multiple_of;
mod manual_midpoint;
mod misrefactored_assign_op;
@ -463,7 +466,7 @@ declare_clippy_lint! {
declare_clippy_lint! {
/// ### What it does
/// Checks for statements of the form `(a - b) < f32::EPSILON` or
/// `(a - b) < f64::EPSILON`. Notes the missing `.abs()`.
/// `(a - b) < f64::EPSILON`. Note the missing `.abs()`.
///
/// ### Why is this bad?
/// The code without `.abs()` is more likely to have a bug.
@ -616,7 +619,7 @@ declare_clippy_lint! {
/// println!("{within_tolerance}"); // true
/// ```
///
/// NB! Do not use `f64::EPSILON` - while the error margin is often called "epsilon", this is
/// NOTE: Do not use `f64::EPSILON` - while the error margin is often called "epsilon", this is
/// a different use of the term that is not suitable for floating point equality comparison.
/// Indeed, for the example above using `f64::EPSILON` as the allowed error would return `false`.
///
@ -679,7 +682,7 @@ declare_clippy_lint! {
/// println!("{within_tolerance}"); // true
/// ```
///
/// NB! Do not use `f64::EPSILON` - while the error margin is often called "epsilon", this is
/// NOTE: Do not use `f64::EPSILON` - while the error margin is often called "epsilon", this is
/// a different use of the term that is not suitable for floating point equality comparison.
/// Indeed, for the example above using `f64::EPSILON` as the allowed error would return `false`.
///
@ -860,6 +863,78 @@ declare_clippy_lint! {
"manual implementation of `.is_multiple_of()`"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for an expression like `(x + (y - 1)) / y` which is a common manual reimplementation
/// of `x.div_ceil(y)`.
///
/// ### Why is this bad?
/// It's simpler, clearer and more readable.
///
/// ### Example
/// ```no_run
/// let x: i32 = 7;
/// let y: i32 = 4;
/// let div = (x + (y - 1)) / y;
/// ```
/// Use instead:
/// ```no_run
/// #![feature(int_roundings)]
/// let x: i32 = 7;
/// let y: i32 = 4;
/// let div = x.div_ceil(y);
/// ```
#[clippy::version = "1.83.0"]
pub MANUAL_DIV_CEIL,
complexity,
"manually reimplementing `div_ceil`"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for comparisons where the relation is always either
/// true or false, but where one side has been upcast so that the comparison is
/// necessary. Only integer types are checked.
///
/// ### Why is this bad?
/// An expression like `let x : u8 = ...; (x as u32) > 300`
/// will mistakenly imply that it is possible for `x` to be outside the range of
/// `u8`.
///
/// ### Example
/// ```no_run
/// let x: u8 = 1;
/// (x as u32) > 300;
/// ```
#[clippy::version = "pre 1.29.0"]
pub INVALID_UPCAST_COMPARISONS,
pedantic,
"a comparison involving an upcast which is always true or false"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for the usage of division (`/`) and remainder (`%`) operations
/// when performed on any integer types using the default `Div` and `Rem` trait implementations.
///
/// ### Why restrict this?
/// In cryptographic contexts, division can result in timing sidechannel vulnerabilities,
/// and needs to be replaced with constant-time code instead (e.g. Barrett reduction).
///
/// ### Example
/// ```no_run
/// let my_div = 10 / 2;
/// ```
/// Use instead:
/// ```no_run
/// let my_div = 10 >> 1;
/// ```
#[clippy::version = "1.79.0"]
pub INTEGER_DIVISION_REMAINDER_USED,
restriction,
"use of disallowed default division and remainder operations"
}
pub struct Operators {
arithmetic_context: numeric_arithmetic::Context,
verbose_bit_mask_threshold: u64,
@ -897,6 +972,7 @@ impl_lint_pass!(Operators => [
FLOAT_EQUALITY_WITHOUT_ABS,
IDENTITY_OP,
INTEGER_DIVISION,
INTEGER_DIVISION_REMAINDER_USED,
CMP_OWNED,
FLOAT_CMP,
FLOAT_CMP_CONST,
@ -906,6 +982,8 @@ impl_lint_pass!(Operators => [
SELF_ASSIGNMENT,
MANUAL_MIDPOINT,
MANUAL_IS_MULTIPLE_OF,
MANUAL_DIV_CEIL,
INVALID_UPCAST_COMPARISONS,
]);
impl<'tcx> LateLintPass<'tcx> for Operators {
@ -921,6 +999,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
}
erasing_op::check(cx, e, op.node, lhs, rhs);
identity_op::check(cx, e, op.node, lhs, rhs);
invalid_upcast_comparisons::check(cx, op.node, lhs, rhs, e.span);
needless_bitwise_bool::check(cx, e, op.node, lhs, rhs);
manual_midpoint::check(cx, e, op.node, lhs, rhs, self.msrv);
manual_is_multiple_of::check(cx, e, op.node, lhs, rhs, self.msrv);
@ -933,6 +1012,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
duration_subsec::check(cx, e, op.node, lhs, rhs);
float_equality_without_abs::check(cx, e, op.node, lhs, rhs);
integer_division::check(cx, e, op.node, lhs, rhs);
integer_division_remainder_used::check(cx, op.node, lhs, rhs, e.span);
cmp_owned::check(cx, op.node, lhs, rhs);
float_cmp::check(cx, e, op.node, lhs, rhs);
modulo_one::check(cx, e, op.node, rhs);
@ -944,6 +1024,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
rhs,
self.modulo_arithmetic_allow_comparison_to_zero,
);
manual_div_ceil::check(cx, e, op.node, lhs, rhs, self.msrv);
},
ExprKind::AssignOp(op, lhs, rhs) => {
let bin_op = op.node.into();

View file

@ -1,4 +1,4 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
use clippy_utils::source::snippet_with_applicability;
use rustc_ast::ast::BinOpKind::{Add, BitAnd, BitOr, BitXor, Div, Mul, Rem, Shl, Shr, Sub};
use rustc_ast::ast::{BinOpKind, Expr, ExprKind};
@ -10,7 +10,8 @@ use rustc_span::source_map::Spanned;
declare_clippy_lint! {
/// ### What it does
/// Checks for operations where precedence may be unclear and suggests to add parentheses.
/// It catches a mixed usage of arithmetic and bit shifting/combining operators without parentheses
/// It catches a mixed usage of arithmetic and bit shifting/combining operators,
/// as well as method calls applied to closures.
///
/// ### Why is this bad?
/// Not everyone knows the precedence of those operators by
@ -109,6 +110,19 @@ impl EarlyLintPass for Precedence {
},
_ => (),
}
} else if let ExprKind::MethodCall(method_call) = &expr.kind
&& let ExprKind::Closure(closure) = &method_call.receiver.kind
{
span_lint_and_then(cx, PRECEDENCE, expr.span, "precedence might not be obvious", |diag| {
diag.multipart_suggestion(
"consider parenthesizing the closure",
vec![
(closure.fn_decl_span.shrink_to_lo(), String::from("(")),
(closure.body.span.shrink_to_hi(), String::from(")")),
],
Applicability::MachineApplicable,
);
});
}
}
}

View file

@ -86,8 +86,7 @@ impl LateLintPass<'_> for SingleRangeInVecInit {
return;
};
let ExprKind::Struct(&qpath, [start, end], StructTailExpr::None) = inner_expr.kind
else {
let ExprKind::Struct(&qpath, [start, end], StructTailExpr::None) = inner_expr.kind else {
return;
};

View file

@ -1,6 +1,7 @@
use clippy_utils::diagnostics::span_lint;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::qpath_generic_tys;
use clippy_utils::res::MaybeResPath;
use clippy_utils::source::snippet;
use rustc_hir::def_id::DefId;
use rustc_hir::{self as hir, QPath};
use rustc_lint::LateContext;
@ -13,12 +14,21 @@ pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, qpath: &QPath<'_
&& let Some(arg) = qpath_generic_tys(qpath).next()
&& arg.basic_res().opt_def_id() == Some(def_id)
{
span_lint(
span_lint_and_then(
cx,
OPTION_OPTION,
hir_ty.span,
"consider using `Option<T>` instead of `Option<Option<T>>` or a custom \
enum if you need to distinguish all 3 cases",
// use just `T` here, as the inner type is not what's problematic
"use of `Option<Option<T>>`",
|diag| {
// but use the specific type here, as:
// - this is kind of a suggestion
// - it's printed right after the linted type
let inner_opt = snippet(cx, arg.span, "_");
diag.help(format!(
"consider using `{inner_opt}`, or a custom enum if you need to distinguish all 3 cases"
));
},
);
true
} else {

View file

@ -454,6 +454,6 @@ fn extend_with_matching(
fn eq_pre_post(ps1: &[Pat], ps2: &[Pat], idx: usize) -> bool {
ps1.len() == ps2.len()
&& ps1[idx].is_rest() == ps2[idx].is_rest() // Avoid `[x, ..] | [x, 0]` => `[x, .. | 0]`.
&& over(&ps1[..idx], &ps2[..idx], |l, r| eq_pat(l, r))
&& over(&ps1[idx + 1..], &ps2[idx + 1..], |l, r| eq_pat(l, r))
&& over(&ps1[..idx], &ps2[..idx], eq_pat)
&& over(&ps1[idx + 1..], &ps2[idx + 1..], eq_pat)
}

View file

@ -9,7 +9,7 @@ use rustc_ast::{
FormatPlaceholder, FormatTrait,
};
use rustc_errors::Applicability;
use rustc_hir::{Expr, Impl, Item, ItemKind};
use rustc_hir::{Expr, Impl, Item, ItemKind, OwnerId};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_session::impl_lint_pass;
use rustc_span::{BytePos, Span};
@ -240,7 +240,8 @@ declare_clippy_lint! {
pub struct Write {
format_args: FormatArgsStorage,
in_debug_impl: bool,
// The outermost `impl Debug` we're currently in. While we're in one, `USE_DEBUG` is deactivated
outermost_debug_impl: Option<OwnerId>,
allow_print_in_tests: bool,
}
@ -248,10 +249,14 @@ impl Write {
pub fn new(conf: &'static Conf, format_args: FormatArgsStorage) -> Self {
Self {
format_args,
in_debug_impl: false,
outermost_debug_impl: None,
allow_print_in_tests: conf.allow_print_in_tests,
}
}
fn in_debug_impl(&self) -> bool {
self.outermost_debug_impl.is_some()
}
}
impl_lint_pass!(Write => [
@ -268,14 +273,16 @@ impl_lint_pass!(Write => [
impl<'tcx> LateLintPass<'tcx> for Write {
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
if is_debug_impl(cx, item) {
self.in_debug_impl = true;
// Only check for `impl Debug`s if we're not already in one
if self.outermost_debug_impl.is_none() && is_debug_impl(cx, item) {
self.outermost_debug_impl = Some(item.owner_id);
}
}
fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
if is_debug_impl(cx, item) {
self.in_debug_impl = false;
fn check_item_post(&mut self, _cx: &LateContext<'_>, item: &Item<'_>) {
// Only clear `self.outermost_debug_impl` if we're escaping the _outermost_ debug impl
if self.outermost_debug_impl == Some(item.owner_id) {
self.outermost_debug_impl = None;
}
}
@ -329,7 +336,7 @@ impl<'tcx> LateLintPass<'tcx> for Write {
check_literal(cx, format_args, name);
if !self.in_debug_impl {
if !self.in_debug_impl() {
for piece in &format_args.template {
if let &FormatArgsPiece::Placeholder(FormatPlaceholder {
span: Some(span),

View file

@ -5,7 +5,7 @@ use clippy_utils::sym;
use rustc_errors::Applicability;
use rustc_hir as hir;
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::{self, EarlyBinder, GenericArgKind};
use rustc_middle::ty::{self, GenericArgKind};
use rustc_session::{declare_lint_pass, declare_tool_lint};
declare_tool_lint! {

View file

@ -1,6 +1,6 @@
[package]
name = "clippy_utils"
version = "0.1.92"
version = "0.1.93"
edition = "2024"
description = "Helpful tools for writing lints, provided as they are used in Clippy"
repository = "https://github.com/rust-lang/rust-clippy"

View file

@ -8,7 +8,7 @@ This crate is only guaranteed to build with this `nightly` toolchain:
<!-- begin autogenerated nightly -->
```
nightly-2025-10-16
nightly-2025-10-31
```
<!-- end autogenerated nightly -->

View file

@ -48,12 +48,10 @@ pub fn eq_pat(l: &Pat, r: &Pat) -> bool {
(Box(l), Box(r))
| (Ref(l, Mutability::Not), Ref(r, Mutability::Not))
| (Ref(l, Mutability::Mut), Ref(r, Mutability::Mut)) => eq_pat(l, r),
(Tuple(l), Tuple(r)) | (Slice(l), Slice(r)) => over(l, r, |l, r| eq_pat(l, r)),
(Tuple(l), Tuple(r)) | (Slice(l), Slice(r)) => over(l, r, eq_pat),
(Path(lq, lp), Path(rq, rp)) => both(lq.as_deref(), rq.as_deref(), eq_qself) && eq_path(lp, rp),
(TupleStruct(lqself, lp, lfs), TupleStruct(rqself, rp, rfs)) => {
eq_maybe_qself(lqself.as_deref(), rqself.as_deref())
&& eq_path(lp, rp)
&& over(lfs, rfs, |l, r| eq_pat(l, r))
eq_maybe_qself(lqself.as_deref(), rqself.as_deref()) && eq_path(lp, rp) && over(lfs, rfs, eq_pat)
},
(Struct(lqself, lp, lfs, lr), Struct(rqself, rp, rfs, rr)) => {
lr == rr
@ -61,7 +59,7 @@ pub fn eq_pat(l: &Pat, r: &Pat) -> bool {
&& eq_path(lp, rp)
&& unordered_over(lfs, rfs, eq_field_pat)
},
(Or(ls), Or(rs)) => unordered_over(ls, rs, |l, r| eq_pat(l, r)),
(Or(ls), Or(rs)) => unordered_over(ls, rs, eq_pat),
(MacCall(l), MacCall(r)) => eq_mac_call(l, r),
_ => false,
}

View file

@ -209,7 +209,7 @@ pub struct Range<'a> {
pub end: Option<&'a Expr<'a>>,
/// Whether the interval is open or closed.
pub limits: ast::RangeLimits,
pub span: Span
pub span: Span,
}
impl<'a> Range<'a> {
@ -236,14 +236,12 @@ impl<'a> Range<'a> {
limits: ast::RangeLimits::HalfOpen,
span,
}),
(hir::LangItem::RangeFrom, [field]) if field.ident.name == sym::start => {
Some(Range {
start: Some(field.expr),
end: None,
limits: ast::RangeLimits::HalfOpen,
span,
})
},
(hir::LangItem::RangeFrom, [field]) if field.ident.name == sym::start => Some(Range {
start: Some(field.expr),
end: None,
limits: ast::RangeLimits::HalfOpen,
span,
}),
(hir::LangItem::Range, [field1, field2]) => {
let (start, end) = match (field1.ident.name, field2.ident.name) {
(sym::start, sym::end) => (field1.expr, field2.expr),
@ -257,14 +255,12 @@ impl<'a> Range<'a> {
span,
})
},
(hir::LangItem::RangeToInclusive, [field]) if field.ident.name == sym::end => {
Some(Range {
start: None,
end: Some(field.expr),
limits: ast::RangeLimits::Closed,
span,
})
},
(hir::LangItem::RangeToInclusive, [field]) if field.ident.name == sym::end => Some(Range {
start: None,
end: Some(field.expr),
limits: ast::RangeLimits::Closed,
span,
}),
(hir::LangItem::RangeTo, [field]) if field.ident.name == sym::end => Some(Range {
start: None,
end: Some(field.expr),

View file

@ -342,7 +342,7 @@ pub fn is_ty_alias(qpath: &QPath<'_>) -> bool {
match *qpath {
QPath::Resolved(_, path) => matches!(path.res, Res::Def(DefKind::TyAlias | DefKind::AssocTy, ..)),
QPath::TypeRelative(ty, _) if let TyKind::Path(qpath) = ty.kind => is_ty_alias(&qpath),
_ => false,
QPath::TypeRelative(..) => false,
}
}

View file

@ -107,9 +107,7 @@ pub trait MaybeQPath<'a>: Copy {
fn f(qpath: &QPath<'_>, id: HirId, typeck: &TypeckResults<'_>) -> Res {
match *qpath {
QPath::Resolved(_, p) => p.res,
QPath::TypeRelative(..) if let Some((kind, id)) = typeck.ty_based_def(id) => {
Res::Def(kind, id)
},
QPath::TypeRelative(..) if let Some((kind, id)) = typeck.ty_based_def(id) => Res::Def(kind, id),
QPath::TypeRelative(..) => Res::Err,
}
}
@ -403,7 +401,7 @@ impl<'a> MaybeResPath<'a> for &QPath<'a> {
fn opt_res_path(self) -> OptResPath<'a> {
match *self {
QPath::Resolved(ty, path) => (ty, Some(path)),
_ => (None, None),
QPath::TypeRelative(..) => (None, None),
}
}
}

View file

@ -13,7 +13,7 @@ use rustc_middle::ty::TyCtxt;
use rustc_session::Session;
use rustc_span::source_map::{SourceMap, original_sp};
use rustc_span::{
BytePos, DesugaringKind, DUMMY_SP, FileNameDisplayPreference, Pos, RelativeBytePos, SourceFile, SourceFileAndLine,
BytePos, DUMMY_SP, DesugaringKind, FileNameDisplayPreference, Pos, RelativeBytePos, SourceFile, SourceFileAndLine,
Span, SpanData, SyntaxContext, hygiene,
};
use std::borrow::Cow;
@ -675,7 +675,7 @@ fn snippet_with_context_sess<'a>(
return (
snippet_with_applicability_sess(sess, span, default, applicability),
false,
)
);
}
let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(

View file

@ -127,7 +127,11 @@ impl<'a> Sugg<'a> {
/// Generate a suggestion for an expression with the given snippet. This is used by the `hir_*`
/// function variants of `Sugg`, since these use different snippet functions.
fn hir_from_snippet(cx: &LateContext<'_>, expr: &hir::Expr<'_>, mut get_snippet: impl FnMut(Span) -> Cow<'a, str>) -> Self {
fn hir_from_snippet(
cx: &LateContext<'_>,
expr: &hir::Expr<'_>,
mut get_snippet: impl FnMut(Span) -> Cow<'a, str>,
) -> Self {
if let Some(range) = higher::Range::hir(cx, expr) {
let op = AssocOp::Range(range.limits);
let start = range.start.map_or("".into(), |expr| get_snippet(expr.span));
@ -765,7 +769,7 @@ pub struct DerefClosure {
/// such as explicit deref and borrowing cases.
/// Returns `None` if no such use cases have been triggered in closure body
///
/// note: this only works on single line immutable closures with exactly one input parameter.
/// note: This only works on immutable closures with exactly one input parameter.
pub fn deref_closure_args(cx: &LateContext<'_>, closure: &hir::Expr<'_>) -> Option<DerefClosure> {
if let ExprKind::Closure(&Closure {
fn_decl, def_id, body, ..

View file

@ -1,6 +1,6 @@
[package]
name = "declare_clippy_lint"
version = "0.1.92"
version = "0.1.93"
edition = "2024"
repository = "https://github.com/rust-lang/rust-clippy"
license = "MIT OR Apache-2.0"

View file

@ -1,6 +1,6 @@
[toolchain]
# begin autogenerated nightly
channel = "nightly-2025-10-16"
channel = "nightly-2025-10-31"
# end autogenerated nightly
components = ["cargo", "llvm-tools", "rust-src", "rust-std", "rustc", "rustc-dev", "rustfmt"]
profile = "minimal"

View file

@ -9,7 +9,7 @@
clippy::no_effect,
clippy::unnecessary_operation,
clippy::never_loop,
clippy::needless_if,
clippy::needless_ifs,
clippy::collapsible_if,
clippy::blocks_in_conditions,
clippy::single_match,

View file

@ -57,3 +57,15 @@ macro_rules! bad_transmute {
std::mem::transmute($e)
};
}
#[macro_export]
#[rustfmt::skip]
macro_rules! double_parens {
($a:expr, $b:expr, $c:expr, $d:expr) => {{
let a = ($a);
let a = (());
let b = ((5));
let c = std::convert::identity((5));
InterruptMask((($a.union($b).union($c).union($d)).into_bits()) as u32)
}};
}

View file

@ -230,3 +230,14 @@ pub fn allow_lint_same_span_derive(input: TokenStream) -> TokenStream {
span_help(Group::new(Delimiter::Brace, TokenStream::new()).into()),
])
}
#[proc_macro_derive(DoubleParens)]
pub fn derive_double_parens(_: TokenStream) -> TokenStream {
quote! {
fn foo() {
let a = (());
let b = ((5));
let c = std::convert::identity((5));
}
}
}

View file

@ -1,5 +1,5 @@
#![feature(proc_macro_span)]
#![allow(clippy::needless_if, dead_code)]
#![allow(clippy::needless_ifs, dead_code)]
extern crate proc_macro;

View file

@ -4,7 +4,7 @@
#![allow(
unused,
unnecessary_transmutes,
clippy::needless_if,
clippy::needless_ifs,
clippy::missing_transmute_annotations
)]
#![warn(clippy::nonminimal_bool)]

View file

@ -4,7 +4,7 @@
#![allow(
unused,
unnecessary_transmutes,
clippy::needless_if,
clippy::needless_ifs,
clippy::missing_transmute_annotations
)]
#![warn(clippy::nonminimal_bool)]

View file

@ -1,4 +1,4 @@
#![allow(non_local_definitions, clippy::needless_if)]
#![allow(non_local_definitions, clippy::needless_ifs)]
#![warn(clippy::bool_comparison)]
#![allow(clippy::non_canonical_partial_ord_impl)]

View file

@ -1,4 +1,4 @@
#![allow(non_local_definitions, clippy::needless_if)]
#![allow(non_local_definitions, clippy::needless_ifs)]
#![warn(clippy::bool_comparison)]
#![allow(clippy::non_canonical_partial_ord_impl)]

View file

@ -1,6 +1,6 @@
#![allow(
unused,
clippy::needless_if,
clippy::needless_ifs,
clippy::redundant_clone,
clippy::derive_partial_eq_without_eq
)] // See #5700

View file

@ -1,6 +1,6 @@
#![allow(
unused,
clippy::needless_if,
clippy::needless_ifs,
clippy::redundant_clone,
clippy::derive_partial_eq_without_eq
)] // See #5700

View file

@ -1,4 +1,4 @@
#![allow(clippy::assertions_on_constants, clippy::equatable_if_let, clippy::needless_if)]
#![allow(clippy::assertions_on_constants, clippy::equatable_if_let, clippy::needless_ifs)]
#![warn(clippy::collapsible_if, clippy::collapsible_else_if)]
#[rustfmt::skip]

View file

@ -1,4 +1,4 @@
#![allow(clippy::assertions_on_constants, clippy::equatable_if_let, clippy::needless_if)]
#![allow(clippy::assertions_on_constants, clippy::equatable_if_let, clippy::needless_ifs)]
#![warn(clippy::collapsible_if, clippy::collapsible_else_if)]
#[rustfmt::skip]

View file

@ -1,7 +1,7 @@
#![allow(
clippy::assertions_on_constants,
clippy::equatable_if_let,
clippy::needless_if,
clippy::needless_ifs,
clippy::nonminimal_bool,
clippy::eq_op,
clippy::redundant_pattern_matching

View file

@ -1,7 +1,7 @@
#![allow(
clippy::assertions_on_constants,
clippy::equatable_if_let,
clippy::needless_if,
clippy::needless_ifs,
clippy::nonminimal_bool,
clippy::eq_op,
clippy::redundant_pattern_matching

View file

@ -1,5 +1,5 @@
#![warn(clippy::comparison_to_empty)]
#![allow(clippy::borrow_deref_ref, clippy::needless_if, clippy::useless_vec)]
#![allow(clippy::borrow_deref_ref, clippy::needless_ifs, clippy::useless_vec)]
fn main() {
// Disallow comparisons to empty

View file

@ -1,5 +1,5 @@
#![warn(clippy::comparison_to_empty)]
#![allow(clippy::borrow_deref_ref, clippy::needless_if, clippy::useless_vec)]
#![allow(clippy::borrow_deref_ref, clippy::needless_ifs, clippy::useless_vec)]
fn main() {
// Disallow comparisons to empty

View file

@ -1,4 +1,4 @@
#![allow(clippy::needless_if)]
#![allow(clippy::needless_ifs)]
#[derive(Default)]
struct A<T> {

View file

@ -1,4 +1,4 @@
#![allow(clippy::needless_if)]
#![allow(clippy::needless_ifs)]
#[derive(Default)]
struct A<T> {

View file

@ -1,7 +1,7 @@
//@aux-build:proc_macros.rs
#![allow(
dead_code,
clippy::needless_if,
clippy::needless_ifs,
clippy::similar_names,
clippy::single_match,
clippy::toplevel_ref_arg,

View file

@ -1,33 +1,14 @@
#![warn(clippy::needless_doctest_main)]
//! issue 10491:
//! ```rust,no_test
//! use std::collections::HashMap;
//!
//! fn main() {
//! let mut m = HashMap::new();
//! m.insert(1u32, 2u32);
//! }
//! ```
/// some description here
/// ```rust,no_test
/// fn main() {
/// foo()
/// }
/// ```
fn foo() {}
#[rustfmt::skip]
/// Description
/// ```rust
/// fn main() {
//~^ error: needless `fn main` in doctest
//~^ needless_doctest_main
/// let a = 0;
/// }
/// ```
fn mulpipulpi() {}
#[rustfmt::skip]
/// With a `#[no_main]`
/// ```rust
/// #[no_main]
@ -45,7 +26,6 @@ fn pulpimulpi() {}
/// ```
fn plumilupi() {}
#[rustfmt::skip]
/// Additional function, shouldn't trigger
/// ```rust
/// fn additional_function() {
@ -58,7 +38,6 @@ fn plumilupi() {}
/// ```
fn mlupipupi() {}
#[rustfmt::skip]
/// Additional function AFTER main, shouldn't trigger
/// ```rust
/// fn main() {
@ -71,22 +50,19 @@ fn mlupipupi() {}
/// ```
fn lumpimupli() {}
#[rustfmt::skip]
/// Ignore code block, should not lint at all
/// ```rust, ignore
/// fn main() {
//~^ error: needless `fn main` in doctest
/// // Hi!
/// let _ = 0;
/// }
/// ```
fn mpulpilumi() {}
#[rustfmt::skip]
/// Spaces in weird positions (including an \u{A0} after `main`)
/// ```rust
/// fn main (){
//~^ error: needless `fn main` in doctest
//~^ needless_doctest_main
/// let _ = 0;
/// }
/// ```

View file

@ -1,36 +1,17 @@
error: needless `fn main` in doctest
--> tests/ui/doc/needless_doctest_main.rs:23:5
--> tests/ui/doc/needless_doctest_main.rs:5:5
|
LL | /// fn main() {
| _____^
LL | |
LL | | /// let a = 0;
LL | | /// }
| |_____^
LL | /// fn main() {
| ^^^^^^^
|
= note: `-D clippy::needless-doctest-main` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::needless_doctest_main)]`
error: needless `fn main` in doctest
--> tests/ui/doc/needless_doctest_main.rs:77:5
--> tests/ui/doc/needless_doctest_main.rs:64:5
|
LL | /// fn main() {
| _____^
LL | |
LL | | /// // Hi!
LL | | /// let _ = 0;
LL | | /// }
| |_____^
LL | /// fn main (){
| ^^^^^^^^^^^
error: needless `fn main` in doctest
--> tests/ui/doc/needless_doctest_main.rs:88:5
|
LL | /// fn main (){
| _____^
LL | |
LL | | /// let _ = 0;
LL | | /// }
| |_____^
error: aborting due to 3 previous errors
error: aborting due to 2 previous errors

View file

@ -1,4 +1,4 @@
#![allow(clippy::needless_if)]
#![allow(clippy::needless_ifs)]
fn main() {
let x = 1;

View file

@ -1,4 +1,4 @@
#![allow(clippy::needless_if)]
#![allow(clippy::needless_ifs)]
fn main() {
let x = 1;

View file

@ -1,8 +1,13 @@
//@aux-build:proc_macros.rs
//@aux-build:proc_macro_derive.rs
//@aux-build:macro_rules.rs
#![warn(clippy::double_parens)]
#![expect(clippy::eq_op, clippy::no_effect)]
#![feature(custom_inner_attributes)]
#![rustfmt::skip]
use proc_macros::{external, with_span};
fn dummy_fn<T>(_: T) {}
struct DummyStruct;
@ -96,4 +101,64 @@ fn issue9000(x: DummyStruct) {
//~^ double_parens
}
fn issue15892() {
use macro_rules::double_parens as double_parens_external;
macro_rules! double_parens{
($a:expr, $b:expr, $c:expr, $d:expr) => {{
let a = ($a);
let a = ();
//~^ double_parens
let b = (5);
//~^ double_parens
let c = std::convert::identity(5);
//~^ double_parens
InterruptMask((($a.union($b).union($c).union($d)).into_bits()) as u32)
}};
}
// Don't lint: external macro
(external!((5)));
external!(((5)));
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct InterruptMask(u32);
impl InterruptMask {
pub const OE: InterruptMask = InterruptMask(1 << 10);
pub const BE: InterruptMask = InterruptMask(1 << 9);
pub const PE: InterruptMask = InterruptMask(1 << 8);
pub const FE: InterruptMask = InterruptMask(1 << 7);
// Lint: internal macro
pub const E: InterruptMask = double_parens!((Self::OE), Self::BE, Self::PE, Self::FE);
// Don't lint: external macro
pub const F: InterruptMask = double_parens_external!((Self::OE), Self::BE, Self::PE, Self::FE);
#[allow(clippy::unnecessary_cast)]
pub const G: InterruptMask = external!(
InterruptMask((((Self::OE.union(Self::BE).union(Self::PE).union(Self::FE))).into_bits()) as u32)
);
#[allow(clippy::unnecessary_cast)]
// Don't lint: external proc-macro
pub const H: InterruptMask = with_span!(span
InterruptMask((((Self::OE.union(Self::BE).union(Self::PE).union(Self::FE))).into_bits()) as u32)
);
pub const fn into_bits(self) -> u32 {
self.0
}
#[must_use]
pub const fn union(self, rhs: Self) -> Self {
InterruptMask(self.0 | rhs.0)
}
}
}
fn issue15940() {
use proc_macro_derive::DoubleParens;
#[derive(DoubleParens)]
// Don't lint: external derive macro
pub struct Person;
}
fn main() {}

View file

@ -1,8 +1,13 @@
//@aux-build:proc_macros.rs
//@aux-build:proc_macro_derive.rs
//@aux-build:macro_rules.rs
#![warn(clippy::double_parens)]
#![expect(clippy::eq_op, clippy::no_effect)]
#![feature(custom_inner_attributes)]
#![rustfmt::skip]
use proc_macros::{external, with_span};
fn dummy_fn<T>(_: T) {}
struct DummyStruct;
@ -96,4 +101,64 @@ fn issue9000(x: DummyStruct) {
//~^ double_parens
}
fn issue15892() {
use macro_rules::double_parens as double_parens_external;
macro_rules! double_parens{
($a:expr, $b:expr, $c:expr, $d:expr) => {{
let a = ($a);
let a = (());
//~^ double_parens
let b = ((5));
//~^ double_parens
let c = std::convert::identity((5));
//~^ double_parens
InterruptMask((($a.union($b).union($c).union($d)).into_bits()) as u32)
}};
}
// Don't lint: external macro
(external!((5)));
external!(((5)));
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct InterruptMask(u32);
impl InterruptMask {
pub const OE: InterruptMask = InterruptMask(1 << 10);
pub const BE: InterruptMask = InterruptMask(1 << 9);
pub const PE: InterruptMask = InterruptMask(1 << 8);
pub const FE: InterruptMask = InterruptMask(1 << 7);
// Lint: internal macro
pub const E: InterruptMask = double_parens!((Self::OE), Self::BE, Self::PE, Self::FE);
// Don't lint: external macro
pub const F: InterruptMask = double_parens_external!((Self::OE), Self::BE, Self::PE, Self::FE);
#[allow(clippy::unnecessary_cast)]
pub const G: InterruptMask = external!(
InterruptMask((((Self::OE.union(Self::BE).union(Self::PE).union(Self::FE))).into_bits()) as u32)
);
#[allow(clippy::unnecessary_cast)]
// Don't lint: external proc-macro
pub const H: InterruptMask = with_span!(span
InterruptMask((((Self::OE.union(Self::BE).union(Self::PE).union(Self::FE))).into_bits()) as u32)
);
pub const fn into_bits(self) -> u32 {
self.0
}
#[must_use]
pub const fn union(self, rhs: Self) -> Self {
InterruptMask(self.0 | rhs.0)
}
}
}
fn issue15940() {
use proc_macro_derive::DoubleParens;
#[derive(DoubleParens)]
// Don't lint: external derive macro
pub struct Person;
}
fn main() {}

Some files were not shown because too many files have changed in this diff Show more