diff options
| author | Laurențiu Nicola <lnicola@users.noreply.github.com> | 2025-06-30 15:11:33 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-30 15:11:33 +0000 |
| commit | c5f4b80cb9a678297548ea13feb2e57696e09e6e (patch) | |
| tree | 69ecc9ed7f6fb830c4814d1085ba96820b4ab864 /src/tools | |
| parent | 863d0b5d53ebd5aa2d3c2c415fff841741c3937c (diff) | |
| parent | 0edf5726c773484b53f9612f687dec916a60883f (diff) | |
| download | rust-c5f4b80cb9a678297548ea13feb2e57696e09e6e.tar.gz rust-c5f4b80cb9a678297548ea13feb2e57696e09e6e.zip | |
Merge pull request #20128 from lnicola/sync-from-rust
Sync from downstream
Diffstat (limited to 'src/tools')
237 files changed, 5523 insertions, 1349 deletions
diff --git a/src/tools/build-manifest/src/main.rs b/src/tools/build-manifest/src/main.rs index 4c53ea42793..0520eff0fa2 100644 --- a/src/tools/build-manifest/src/main.rs +++ b/src/tools/build-manifest/src/main.rs @@ -14,6 +14,7 @@ use crate::versions::{PkgType, Versions}; static HOSTS: &[&str] = &[ "aarch64-apple-darwin", + "aarch64-pc-windows-gnullvm", "aarch64-pc-windows-msvc", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", @@ -44,6 +45,7 @@ static HOSTS: &[&str] = &[ "x86_64-apple-darwin", "x86_64-pc-solaris", "x86_64-pc-windows-gnu", + "x86_64-pc-windows-gnullvm", "x86_64-pc-windows-msvc", "x86_64-unknown-freebsd", "x86_64-unknown-illumos", @@ -470,7 +472,7 @@ impl Builder { } // so is rust-mingw if it's available for the target PkgType::RustMingw => { - if host.contains("pc-windows-gnu") { + if host.ends_with("pc-windows-gnu") { components.push(host_component(pkg)); } } diff --git a/src/tools/cargo b/src/tools/cargo -Subproject 84709f085062cbf3c51fa507527c1b233401517 +Subproject 930b4f62cfcd1f0eabdb30a56d91bf6844b739b diff --git a/src/tools/clippy/.github/ISSUE_TEMPLATE/new_lint.yml b/src/tools/clippy/.github/ISSUE_TEMPLATE/new_lint.yml index b49493edce1..464740640e0 100644 --- a/src/tools/clippy/.github/ISSUE_TEMPLATE/new_lint.yml +++ b/src/tools/clippy/.github/ISSUE_TEMPLATE/new_lint.yml @@ -1,5 +1,7 @@ name: New lint suggestion -description: Suggest a new Clippy lint. +description: | + Suggest a new Clippy lint (currently not accepting new lints) + Check out the Clippy book for more information about the feature freeze. labels: ["A-lint"] body: - type: markdown diff --git a/src/tools/clippy/.github/PULL_REQUEST_TEMPLATE.md b/src/tools/clippy/.github/PULL_REQUEST_TEMPLATE.md index 9e49f60892d..83bfd8e9c68 100644 --- a/src/tools/clippy/.github/PULL_REQUEST_TEMPLATE.md +++ b/src/tools/clippy/.github/PULL_REQUEST_TEMPLATE.md @@ -32,6 +32,10 @@ order to get feedback. Delete this line and everything above before opening your PR. +Note that we are currently not taking in new PRs that add new lints. We are in a +feature freeze. Check out the book for more information. If you open a +feature-adding pull request, its review will be delayed. + --- *Please write a short comment explaining your change (or "none" for internal only changes)* diff --git a/src/tools/clippy/.github/workflows/feature_freeze.yml b/src/tools/clippy/.github/workflows/feature_freeze.yml new file mode 100644 index 00000000000..a5f8d4bc145 --- /dev/null +++ b/src/tools/clippy/.github/workflows/feature_freeze.yml @@ -0,0 +1,25 @@ +name: Feature freeze check + +on: + pull_request: + paths: + - 'clippy_lints/src/declared_lints.rs' + +jobs: + auto-comment: + runs-on: ubuntu-latest + + steps: + - name: Check PR Changes + id: pr-changes + run: echo "::set-output name=changes::${{ toJson(github.event.pull_request.changed_files) }}" + + - name: Create Comment + if: steps.pr-changes.outputs.changes != '[]' + run: | + # Use GitHub API to create a comment on the PR + PR_NUMBER=${{ github.event.pull_request.number }} + COMMENT="**Seems that you are trying to add a new lint!**\nWe are currently in a [feature freeze](https://doc.rust-lang.org/nightly/clippy/development/feature_freeze.html), so we are delaying all lint-adding PRs to August 1st and focusing on bugfixes.\nThanks a lot for your contribution, and sorry for the inconvenience.\nWith ❤ from the Clippy team" + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + COMMENT_URL="https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" + curl -s -H "Authorization: token ${GITHUB_TOKEN}" -X POST $COMMENT_URL -d "{\"body\":\"$COMMENT\"}" diff --git a/src/tools/clippy/CHANGELOG.md b/src/tools/clippy/CHANGELOG.md index 0cfe89ad378..a92fbdc767b 100644 --- a/src/tools/clippy/CHANGELOG.md +++ b/src/tools/clippy/CHANGELOG.md @@ -6,7 +6,94 @@ document. ## Unreleased / Beta / In Rust Nightly -[1e5237f4...master](https://github.com/rust-lang/rust-clippy/compare/1e5237f4...master) +[03a5b6b9...master](https://github.com/rust-lang/rust-clippy/compare/03a5b6b9...master) + +## Rust 1.88 + +Current stable, released 2025-06-26 + +[View all 126 merged pull requests](https://github.com/rust-lang/rust-clippy/pulls?q=merged%3A2025-03-21T10%3A30%3A57Z..2025-05-01T08%3A03%3A26Z+base%3Amaster) + +### New Lints + +* Added [`swap_with_temporary`] to `complexity` [#14046](https://github.com/rust-lang/rust-clippy/pull/14046) +* Added [`redundant_test_prefix`] to `restriction` [#13710](https://github.com/rust-lang/rust-clippy/pull/13710) +* Added [`manual_dangling_ptr`] to `style` [#14107](https://github.com/rust-lang/rust-clippy/pull/14107) +* Added [`char_indices_as_byte_indices`] to `correctness` [#13435](https://github.com/rust-lang/rust-clippy/pull/13435) +* Added [`manual_abs_diff`] to `complexity` [#14482](https://github.com/rust-lang/rust-clippy/pull/14482) +* Added [`ignore_without_reason`] to `pedantic` [#13931](https://github.com/rust-lang/rust-clippy/pull/13931) + +### Moves and Deprecations + +* Moved [`uninlined_format_args`] to `style` (from `pedantic`) + [#14160](https://github.com/rust-lang/rust-clippy/pull/14160) +* [`match_on_vec_items`] deprecated in favor of [`indexing_slicing`] + [#14217](https://github.com/rust-lang/rust-clippy/pull/14217) +* Removed superseded lints: `transmute_float_to_int`, `transmute_int_to_char`, + `transmute_int_to_float`, `transmute_num_to_bytes` (now in rustc) + [#14703](https://github.com/rust-lang/rust-clippy/pull/14703) + +### Enhancements + +* Configuration renamed from `lint-inconsistent-struct-field-initializers` + to `check-inconsistent-struct-field-initializers` + [#14280](https://github.com/rust-lang/rust-clippy/pull/14280) +* Paths in `disallowed_*` configurations are now validated + [#14397](https://github.com/rust-lang/rust-clippy/pull/14397) +* [`borrow_as_ptr`] now lints implicit casts as well + [#14408](https://github.com/rust-lang/rust-clippy/pull/14408) +* [`iter_kv_map`] now recognizes references on maps + [#14596](https://github.com/rust-lang/rust-clippy/pull/14596) +* [`empty_enum_variants_with_brackets`] no longer lints reachable enums or enums used + as functions within same crate [#12971](https://github.com/rust-lang/rust-clippy/pull/12971) +* [`needless_lifetimes`] now checks for lifetime uses in closures + [#14608](https://github.com/rust-lang/rust-clippy/pull/14608) +* [`wildcard_imports`] now lints on `pub use` when `warn_on_all_wildcard_imports` is enabled + [#14182](https://github.com/rust-lang/rust-clippy/pull/14182) +* [`collapsible_if`] now recognizes the `let_chains` feature + [#14481](https://github.com/rust-lang/rust-clippy/pull/14481) +* [`match_single_binding`] now allows macros in scrutinee and patterns + [#14635](https://github.com/rust-lang/rust-clippy/pull/14635) +* [`needless_borrow`] does not contradict the compiler's + `dangerous_implicit_autorefs` lint even though the references + are not mandatory + [#14810](https://github.com/rust-lang/rust-clippy/pull/14810) + +### False Positive Fixes + +* [`double_ended_iterator_last`] and [`needless_collect`] fixed FP when iter has side effects + [#14490](https://github.com/rust-lang/rust-clippy/pull/14490) +* [`mut_from_ref`] fixed FP where lifetimes nested in types were not considered + [#14471](https://github.com/rust-lang/rust-clippy/pull/14471) +* [`redundant_clone`] fixed FP in overlapping lifetime + [#14237](https://github.com/rust-lang/rust-clippy/pull/14237) +* [`map_entry`] fixed FP where lint would trigger without insert calls present + [#14568](https://github.com/rust-lang/rust-clippy/pull/14568) +* [`iter_cloned_collect`] fixed FP with custom `From`/`IntoIterator` impl + [#14473](https://github.com/rust-lang/rust-clippy/pull/14473) +* [`shadow_unrelated`] fixed FP in destructuring assignments + [#14381](https://github.com/rust-lang/rust-clippy/pull/14381) +* [`redundant_clone`] fixed FP on enum cast + [#14395](https://github.com/rust-lang/rust-clippy/pull/14395) +* [`collapsible_if`] fixed FP on block stmt before expr + [#14730](https://github.com/rust-lang/rust-clippy/pull/14730) + +### ICE Fixes + +* [`missing_const_for_fn`] fix ICE with `-Z validate-mir` compilation option + [#14776](https://github.com/rust-lang/rust-clippy/pull/14776) + +### Documentation Improvements + +* [`missing_asserts_for_indexing`] improved documentation and examples + [#14108](https://github.com/rust-lang/rust-clippy/pull/14108) + +### Others + +* We're testing with edition 2024 now + [#14602](https://github.com/rust-lang/rust-clippy/pull/14602) +* Don't warn about unloaded crates in `clippy.toml` disallowed paths + [#14733](https://github.com/rust-lang/rust-clippy/pull/14733) ## Rust 1.87 @@ -5729,6 +5816,7 @@ Released 2018-09-13 [`disallowed_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_type [`disallowed_types`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types [`diverging_sub_expression`]: https://rust-lang.github.io/rust-clippy/master/index.html#diverging_sub_expression +[`doc_broken_link`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_broken_link [`doc_comment_double_space_linebreaks`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_comment_double_space_linebreaks [`doc_include_without_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_include_without_cfg [`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation @@ -5967,6 +6055,7 @@ Released 2018-09-13 [`manual_is_ascii_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check [`manual_is_finite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_finite [`manual_is_infinite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_infinite +[`manual_is_multiple_of`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of [`manual_is_power_of_two`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_power_of_two [`manual_is_variant_and`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_variant_and [`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else diff --git a/src/tools/clippy/Cargo.toml b/src/tools/clippy/Cargo.toml index 13cf82a062b..1278427b5a7 100644 --- a/src/tools/clippy/Cargo.toml +++ b/src/tools/clippy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clippy" -version = "0.1.89" +version = "0.1.90" description = "A bunch of helpful lints to avoid common pitfalls in Rust" repository = "https://github.com/rust-lang/rust-clippy" readme = "README.md" @@ -24,6 +24,7 @@ path = "src/driver.rs" clippy_config = { path = "clippy_config" } clippy_lints = { path = "clippy_lints" } clippy_utils = { path = "clippy_utils" } +declare_clippy_lint = { path = "declare_clippy_lint" } rustc_tools_util = { path = "rustc_tools_util", version = "0.4.2" } clippy_lints_internal = { path = "clippy_lints_internal", optional = true } tempfile = { version = "3.20", optional = true } diff --git a/src/tools/clippy/book/src/README.md b/src/tools/clippy/book/src/README.md index 5d2c3972b06..db73b49ecc2 100644 --- a/src/tools/clippy/book/src/README.md +++ b/src/tools/clippy/book/src/README.md @@ -1,5 +1,9 @@ # Clippy +[### IMPORTANT NOTE FOR CONTRIBUTORS ================](development/feature_freeze.md) + +---- + [](https://github.com/rust-lang/rust-clippy#license) A collection of lints to catch common mistakes and improve your diff --git a/src/tools/clippy/book/src/SUMMARY.md b/src/tools/clippy/book/src/SUMMARY.md index 39fe7358ed8..b66c3481e49 100644 --- a/src/tools/clippy/book/src/SUMMARY.md +++ b/src/tools/clippy/book/src/SUMMARY.md @@ -13,6 +13,7 @@ - [GitLab CI](continuous_integration/gitlab.md) - [Travis CI](continuous_integration/travis.md) - [Development](development/README.md) + - [IMPORTANT: FEATURE FREEZE](development/feature_freeze.md) - [Basics](development/basics.md) - [Adding Lints](development/adding_lints.md) - [Defining Lints](development/defining_lints.md) diff --git a/src/tools/clippy/book/src/development/adding_lints.md b/src/tools/clippy/book/src/development/adding_lints.md index 2b89e94cf8f..a42a2983744 100644 --- a/src/tools/clippy/book/src/development/adding_lints.md +++ b/src/tools/clippy/book/src/development/adding_lints.md @@ -1,5 +1,8 @@ # Adding a new lint +[### IMPORTANT NOTE FOR CONTRIBUTORS ================](feature_freeze.md) + + You are probably here because you want to add a new lint to Clippy. If this is the first time you're contributing to Clippy, this document guides you through creating an example lint from scratch. diff --git a/src/tools/clippy/book/src/development/feature_freeze.md b/src/tools/clippy/book/src/development/feature_freeze.md new file mode 100644 index 00000000000..260cb136cc0 --- /dev/null +++ b/src/tools/clippy/book/src/development/feature_freeze.md @@ -0,0 +1,55 @@ +# IMPORTANT: FEATURE FREEZE + +This is a temporary notice. + +From the 26th of June until the 18th of September we will perform a feature freeze. Only bugfix PRs will be reviewed +except already open ones. Every feature-adding PR opened in between those dates will be moved into a +milestone to be reviewed separately at another time. + +We do this because of the long backlog of bugs that need to be addressed +in order to continue being the state-of-the-art linter that Clippy has become known for being. + +## For contributors + +If you are a contributor or are planning to become one, **please do not open a lint-adding PR**, we have lots of open +bugs of all levels of difficulty that you can address instead! + +We currently have about 800 lints, each one posing a maintainability challenge that needs to account to every possible +use case of the whole ecosystem. Bugs are natural in every software, but the Clippy team considers that Clippy needs a +refinement period. + +If you open a PR at this time, we will not review it but push it into a milestone until the refinement period ends, +adding additional load into our reviewing schedules. + +## I want to help, what can I do + +Thanks a lot to everyone who wants to help Clippy become better software in this feature freeze period! +If you'd like to help, making a bugfix, making sure that it works, and opening a PR is a great step! + +To find things to fix, go to the [tracking issue][tracking_issue], find an issue that you like, go there and claim that +issue with `@rustbot claim`. + +As a general metric and always taking into account your skill and knowledge level, you can use this guide: + +- 🟥 [ICEs][search_ice], these are compiler errors that causes Clippy to panic and crash. Usually involves high-level +debugging, sometimes interacting directly with the upstream compiler. Difficult to fix but a great challenge that +improves a lot developer workflows! + +- đźź§ [Suggestion causes bug][sugg_causes_bug], Clippy suggested code that changed logic in some silent way. +Unacceptable, as this may have disastrous consequences. Easier to fix than ICEs + +- 🟨 [Suggestion causes error][sugg_causes_error], Clippy suggested code snippet that caused a compiler error +when applied. We need to make sure that Clippy doesn't suggest using a variable twice at the same time or similar +easy-to-happen occurrences. + +- đźź© [False positives][false_positive], a lint should not have fired, the easiest of them all, as this is "just" +identifying the root of a false positive and making an exception for those cases. + +Note that false negatives do not have priority unless the case is very clear, as they are a feature-request in a +trench coat. + +[search_ice]: https://github.com/rust-lang/rust-clippy/issues?q=sort%3Aupdated-desc+state%3Aopen+label%3A%22I-ICE%22 +[sugg_causes_bug]: https://github.com/rust-lang/rust-clippy/issues?q=sort%3Aupdated-desc%20state%3Aopen%20label%3AI-suggestion-causes-bug +[sugg_causes_error]: https://github.com/rust-lang/rust-clippy/issues?q=sort%3Aupdated-desc%20state%3Aopen%20label%3AI-suggestion-causes-error%20 +[false_positive]: https://github.com/rust-lang/rust-clippy/issues?q=sort%3Aupdated-desc%20state%3Aopen%20label%3AI-false-positive +[tracking_issue]: https://github.com/rust-lang/rust-clippy/issues/15086 diff --git a/src/tools/clippy/book/src/lint_configuration.md b/src/tools/clippy/book/src/lint_configuration.md index 7c850b4b023..e9b7f42a183 100644 --- a/src/tools/clippy/book/src/lint_configuration.md +++ b/src/tools/clippy/book/src/lint_configuration.md @@ -488,6 +488,13 @@ The maximum cognitive complexity a function can have ## `disallowed-macros` The list of disallowed macros, written as fully qualified paths. +**Fields:** +- `path` (required): the fully qualified path to the macro that should be disallowed +- `reason` (optional): explanation why this macro is disallowed +- `replacement` (optional): suggested alternative macro +- `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + if the path doesn't exist, instead of emitting an error + **Default Value:** `[]` --- @@ -498,6 +505,13 @@ The list of disallowed macros, written as fully qualified paths. ## `disallowed-methods` The list of disallowed methods, written as fully qualified paths. +**Fields:** +- `path` (required): the fully qualified path to the method that should be disallowed +- `reason` (optional): explanation why this method is disallowed +- `replacement` (optional): suggested alternative method +- `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + if the path doesn't exist, instead of emitting an error + **Default Value:** `[]` --- @@ -520,6 +534,13 @@ default configuration of Clippy. By default, any configuration will replace the ## `disallowed-types` The list of disallowed types, written as fully qualified paths. +**Fields:** +- `path` (required): the fully qualified path to the type that should be disallowed +- `reason` (optional): explanation why this type is disallowed +- `replacement` (optional): suggested alternative type +- `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + if the path doesn't exist, instead of emitting an error + **Default Value:** `[]` --- @@ -651,13 +672,14 @@ The maximum size of the `Err`-variant in a `Result` returned from a function ## `lint-commented-code` -Whether collapsible `if` chains are linted if they contain comments inside the parts +Whether collapsible `if` and `else if` chains are linted if they contain comments inside the parts that would be collapsed. **Default Value:** `false` --- **Affected lints:** +* [`collapsible_else_if`](https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_else_if) * [`collapsible_if`](https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if) diff --git a/src/tools/clippy/clippy_config/Cargo.toml b/src/tools/clippy/clippy_config/Cargo.toml index 0606245f990..858366c8a5c 100644 --- a/src/tools/clippy/clippy_config/Cargo.toml +++ b/src/tools/clippy/clippy_config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clippy_config" -version = "0.1.89" +version = "0.1.90" edition = "2024" publish = false diff --git a/src/tools/clippy/clippy_config/src/conf.rs b/src/tools/clippy/clippy_config/src/conf.rs index 87158cec42b..841facdca06 100644 --- a/src/tools/clippy/clippy_config/src/conf.rs +++ b/src/tools/clippy/clippy_config/src/conf.rs @@ -575,10 +575,24 @@ define_Conf! { #[conf_deprecated("Please use `cognitive-complexity-threshold` instead", cognitive_complexity_threshold)] cyclomatic_complexity_threshold: u64 = 25, /// The list of disallowed macros, written as fully qualified paths. + /// + /// **Fields:** + /// - `path` (required): the fully qualified path to the macro that should be disallowed + /// - `reason` (optional): explanation why this macro is disallowed + /// - `replacement` (optional): suggested alternative macro + /// - `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + /// if the path doesn't exist, instead of emitting an error #[disallowed_paths_allow_replacements = true] #[lints(disallowed_macros)] disallowed_macros: Vec<DisallowedPath> = Vec::new(), /// The list of disallowed methods, written as fully qualified paths. + /// + /// **Fields:** + /// - `path` (required): the fully qualified path to the method that should be disallowed + /// - `reason` (optional): explanation why this method is disallowed + /// - `replacement` (optional): suggested alternative method + /// - `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + /// if the path doesn't exist, instead of emitting an error #[disallowed_paths_allow_replacements = true] #[lints(disallowed_methods)] disallowed_methods: Vec<DisallowedPath> = Vec::new(), @@ -588,6 +602,13 @@ define_Conf! { #[lints(disallowed_names)] disallowed_names: Vec<String> = DEFAULT_DISALLOWED_NAMES.iter().map(ToString::to_string).collect(), /// The list of disallowed types, written as fully qualified paths. + /// + /// **Fields:** + /// - `path` (required): the fully qualified path to the type that should be disallowed + /// - `reason` (optional): explanation why this type is disallowed + /// - `replacement` (optional): suggested alternative type + /// - `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + /// if the path doesn't exist, instead of emitting an error #[disallowed_paths_allow_replacements = true] #[lints(disallowed_types)] disallowed_types: Vec<DisallowedPath> = Vec::new(), @@ -641,9 +662,9 @@ define_Conf! { /// The maximum size of the `Err`-variant in a `Result` returned from a function #[lints(result_large_err)] large_error_threshold: u64 = 128, - /// Whether collapsible `if` chains are linted if they contain comments inside the parts + /// Whether collapsible `if` and `else if` chains are linted if they contain comments inside the parts /// that would be collapsed. - #[lints(collapsible_if)] + #[lints(collapsible_else_if, collapsible_if)] lint_commented_code: bool = false, /// Whether to suggest reordering constructor fields when initializers are present. /// DEPRECATED CONFIGURATION: lint-inconsistent-struct-field-initializers diff --git a/src/tools/clippy/clippy_dev/src/lint.rs b/src/tools/clippy/clippy_dev/src/lint.rs index e0e036757d5..0d66f167a38 100644 --- a/src/tools/clippy/clippy_dev/src/lint.rs +++ b/src/tools/clippy/clippy_dev/src/lint.rs @@ -13,7 +13,7 @@ pub fn run<'a>(path: &str, edition: &str, args: impl Iterator<Item = &'a String> if is_file { exit_if_err( - Command::new(env::var("CARGO").unwrap_or("cargo".into())) + Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".into())) .args(["run", "--bin", "clippy-driver", "--"]) .args(["-L", "./target/debug"]) .args(["-Z", "no-codegen"]) @@ -26,7 +26,7 @@ pub fn run<'a>(path: &str, edition: &str, args: impl Iterator<Item = &'a String> ); } else { exit_if_err( - Command::new(env::var("CARGO").unwrap_or("cargo".into())) + Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".into())) .arg("build") .status(), ); diff --git a/src/tools/clippy/clippy_dev/src/release.rs b/src/tools/clippy/clippy_dev/src/release.rs index 62c1bee8185..15392dd1d29 100644 --- a/src/tools/clippy/clippy_dev/src/release.rs +++ b/src/tools/clippy/clippy_dev/src/release.rs @@ -5,6 +5,7 @@ static CARGO_TOML_FILES: &[&str] = &[ "clippy_config/Cargo.toml", "clippy_lints/Cargo.toml", "clippy_utils/Cargo.toml", + "declare_clippy_lint/Cargo.toml", "Cargo.toml", ]; diff --git a/src/tools/clippy/clippy_dev/src/serve.rs b/src/tools/clippy/clippy_dev/src/serve.rs index a2d1236629f..498ffeba9d6 100644 --- a/src/tools/clippy/clippy_dev/src/serve.rs +++ b/src/tools/clippy/clippy_dev/src/serve.rs @@ -28,7 +28,7 @@ pub fn run(port: u16, lint: Option<String>) -> ! { .map(mtime); if times.iter().any(|&time| index_time < time) { - Command::new(env::var("CARGO").unwrap_or("cargo".into())) + Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".into())) .arg("collect-metadata") .spawn() .unwrap() diff --git a/src/tools/clippy/clippy_dev/src/update_lints.rs b/src/tools/clippy/clippy_dev/src/update_lints.rs index 08592f2521f..5f6e874ffe2 100644 --- a/src/tools/clippy/clippy_dev/src/update_lints.rs +++ b/src/tools/clippy/clippy_dev/src/update_lints.rs @@ -4,8 +4,9 @@ use crate::utils::{ use itertools::Itertools; use std::collections::HashSet; use std::fmt::Write; +use std::fs; use std::ops::Range; -use std::path::{Path, PathBuf}; +use std::path::{self, Path, PathBuf}; use walkdir::{DirEntry, WalkDir}; const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\ @@ -36,123 +37,164 @@ pub fn generate_lint_files( deprecated: &[DeprecatedLint], renamed: &[RenamedLint], ) { - FileUpdater::default().update_files_checked( + let mut updater = FileUpdater::default(); + updater.update_file_checked( "cargo dev update_lints", update_mode, - &mut [ - ( - "README.md", - &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| { - write!(dst, "{}", round_to_fifty(lints.len())).unwrap(); - }), - ), - ( - "book/src/README.md", - &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| { - write!(dst, "{}", round_to_fifty(lints.len())).unwrap(); - }), - ), - ( - "CHANGELOG.md", - &mut update_text_region_fn( - "<!-- begin autogenerated links to lint list -->\n", - "<!-- end autogenerated links to lint list -->", - |dst| { - for lint in lints - .iter() - .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() - { - writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap(); - } - }, - ), - ), - ( - "clippy_lints/src/lib.rs", - &mut update_text_region_fn( - "// begin lints modules, do not remove this comment, it's used in `update_lints`\n", - "// end lints modules, do not remove this comment, it's used in `update_lints`", - |dst| { - for lint_mod in lints.iter().map(|l| &l.module).sorted().dedup() { - writeln!(dst, "mod {lint_mod};").unwrap(); - } - }, - ), - ), - ("clippy_lints/src/declared_lints.rs", &mut |_, src, dst| { - dst.push_str(GENERATED_FILE_COMMENT); - dst.push_str("pub static LINTS: &[&crate::LintInfo] = &[\n"); - for (module_name, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() { - writeln!(dst, " crate::{module_name}::{lint_name}_INFO,").unwrap(); - } - dst.push_str("];\n"); - UpdateStatus::from_changed(src != dst) - }), - ("clippy_lints/src/deprecated_lints.rs", &mut |_, src, dst| { - let mut searcher = RustSearcher::new(src); - assert!( - searcher.find_token(Token::Ident("declare_with_version")) - && searcher.find_token(Token::Ident("declare_with_version")), - "error reading deprecated lints" - ); - dst.push_str(&src[..searcher.pos() as usize]); - dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n"); - for lint in deprecated { - write!( - dst, - " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", - lint.version, lint.name, lint.reason, - ) - .unwrap(); + "README.md", + &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| { + write!(dst, "{}", round_to_fifty(lints.len())).unwrap(); + }), + ); + updater.update_file_checked( + "cargo dev update_lints", + update_mode, + "book/src/README.md", + &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| { + write!(dst, "{}", round_to_fifty(lints.len())).unwrap(); + }), + ); + updater.update_file_checked( + "cargo dev update_lints", + update_mode, + "CHANGELOG.md", + &mut update_text_region_fn( + "<!-- begin autogenerated links to lint list -->\n", + "<!-- end autogenerated links to lint list -->", + |dst| { + for lint in lints + .iter() + .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() + { + writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap(); } - dst.push_str( - "]}\n\n\ + }, + ), + ); + updater.update_file_checked( + "cargo dev update_lints", + update_mode, + "clippy_lints/src/deprecated_lints.rs", + &mut |_, src, dst| { + let mut searcher = RustSearcher::new(src); + assert!( + searcher.find_token(Token::Ident("declare_with_version")) + && searcher.find_token(Token::Ident("declare_with_version")), + "error reading deprecated lints" + ); + dst.push_str(&src[..searcher.pos() as usize]); + dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n"); + for lint in deprecated { + write!( + dst, + " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", + lint.version, lint.name, lint.reason, + ) + .unwrap(); + } + dst.push_str( + "]}\n\n\ #[rustfmt::skip]\n\ declare_with_version! { RENAMED(RENAMED_VERSION) = [\n\ ", - ); - for lint in renamed { - write!( - dst, - " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", - lint.version, lint.old_name, lint.new_name, - ) - .unwrap(); + ); + for lint in renamed { + write!( + dst, + " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", + lint.version, lint.old_name, lint.new_name, + ) + .unwrap(); + } + dst.push_str("]}\n"); + UpdateStatus::from_changed(src != dst) + }, + ); + updater.update_file_checked( + "cargo dev update_lints", + update_mode, + "tests/ui/deprecated.rs", + &mut |_, src, dst| { + dst.push_str(GENERATED_FILE_COMMENT); + for lint in deprecated { + writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.name, lint.name).unwrap(); + } + dst.push_str("\nfn main() {}\n"); + UpdateStatus::from_changed(src != dst) + }, + ); + updater.update_file_checked( + "cargo dev update_lints", + update_mode, + "tests/ui/rename.rs", + &mut move |_, src, dst| { + let mut seen_lints = HashSet::new(); + 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) { + writeln!(dst, "#![allow({})]", lint.new_name).unwrap(); } - dst.push_str("]}\n"); - UpdateStatus::from_changed(src != dst) - }), - ("tests/ui/deprecated.rs", &mut |_, src, dst| { - dst.push_str(GENERATED_FILE_COMMENT); - for lint in deprecated { - writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.name, lint.name).unwrap(); + } + seen_lints.clear(); + for lint in renamed { + if seen_lints.insert(&lint.old_name) { + writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap(); } - dst.push_str("\nfn main() {}\n"); - UpdateStatus::from_changed(src != dst) - }), - ("tests/ui/rename.rs", &mut move |_, src, dst| { - let mut seen_lints = HashSet::new(); - 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) { - writeln!(dst, "#![allow({})]", lint.new_name).unwrap(); + } + dst.push_str("\nfn main() {}\n"); + UpdateStatus::from_changed(src != dst) + }, + ); + for (crate_name, lints) in lints.iter().into_group_map_by(|&l| { + let Some(path::Component::Normal(name)) = l.path.components().next() else { + // All paths should start with `{crate_name}/src` when parsed from `find_lint_decls` + panic!("internal error: can't read crate name from path `{}`", l.path.display()); + }; + name + }) { + updater.update_file_checked( + "cargo dev update_lints", + update_mode, + Path::new(crate_name).join("src/lib.rs"), + &mut update_text_region_fn( + "// begin lints modules, do not remove this comment, it's used in `update_lints`\n", + "// end lints modules, do not remove this comment, it's used in `update_lints`", + |dst| { + 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)) + .sorted() + .dedup() + { + writeln!(dst, "mod {lint_mod};").unwrap(); } - } - seen_lints.clear(); - for lint in renamed { - if seen_lints.insert(&lint.old_name) { - writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap(); + }, + ), + ); + updater.update_file_checked( + "cargo dev update_lints", + update_mode, + Path::new(crate_name).join("src/declared_lints.rs"), + &mut |_, src, dst| { + dst.push_str(GENERATED_FILE_COMMENT); + dst.push_str("pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[\n"); + for (module_path, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() { + if module_path.is_empty() { + writeln!(dst, " crate::{lint_name}_INFO,").unwrap(); + } else { + writeln!(dst, " crate::{module_path}::{lint_name}_INFO,").unwrap(); } } - dst.push_str("\nfn main() {}\n"); + dst.push_str("];\n"); UpdateStatus::from_changed(src != dst) - }), - ], - ); + }, + ); + } } fn round_to_fifty(count: usize) -> usize { @@ -186,13 +228,25 @@ pub struct RenamedLint { pub fn find_lint_decls() -> Vec<Lint> { let mut lints = Vec::with_capacity(1000); let mut contents = String::new(); - for (file, module) in read_src_with_module("clippy_lints/src".as_ref()) { - parse_clippy_lint_decls( - file.path(), - File::open_read_to_cleared_string(file.path(), &mut contents), - &module, - &mut lints, - ); + 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 @@ -204,7 +258,7 @@ fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator<Item = (DirE 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("clippy_lints/src/".len()..) + && let Some(path) = path.get(src_root.as_os_str().len() + 1..) { if path == b"lib" { Some((e, String::new())) @@ -332,17 +386,13 @@ pub fn read_deprecated_lints() -> (Vec<DeprecatedLint>, Vec<RenamedLint>) { /// Removes the line splices and surrounding quotes from a string literal fn parse_str_lit(s: &str) -> String { - let (s, mode) = if let Some(s) = s.strip_prefix("r") { - (s.trim_matches('#'), rustc_literal_escaper::Mode::RawStr) - } else { - (s, rustc_literal_escaper::Mode::Str) - }; + 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_unicode(s, mode, &mut |_, ch| { + rustc_literal_escaper::unescape_str(s, &mut |_, ch| { if let Ok(ch) = ch { res.push(ch); } diff --git a/src/tools/clippy/clippy_dev/src/utils.rs b/src/tools/clippy/clippy_dev/src/utils.rs index c4808b7048b..89962a11034 100644 --- a/src/tools/clippy/clippy_dev/src/utils.rs +++ b/src/tools/clippy/clippy_dev/src/utils.rs @@ -383,21 +383,6 @@ impl FileUpdater { self.update_file_checked_inner(tool, mode, path.as_ref(), update); } - #[expect(clippy::type_complexity)] - pub fn update_files_checked( - &mut self, - tool: &str, - mode: UpdateMode, - files: &mut [( - impl AsRef<Path>, - &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, - )], - ) { - for (path, update) in files { - self.update_file_checked_inner(tool, mode, path.as_ref(), update); - } - } - pub fn update_file( &mut self, path: impl AsRef<Path>, diff --git a/src/tools/clippy/clippy_lints/Cargo.toml b/src/tools/clippy/clippy_lints/Cargo.toml index 39e4e2e365e..c03cc99b581 100644 --- a/src/tools/clippy/clippy_lints/Cargo.toml +++ b/src/tools/clippy/clippy_lints/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clippy_lints" -version = "0.1.89" +version = "0.1.90" description = "A bunch of helpful lints to avoid common pitfalls in Rust" repository = "https://github.com/rust-lang/rust-clippy" readme = "README.md" @@ -13,6 +13,7 @@ arrayvec = { version = "0.7", default-features = false } cargo_metadata = "0.18" clippy_config = { path = "../clippy_config" } clippy_utils = { path = "../clippy_utils" } +declare_clippy_lint = { path = "../declare_clippy_lint" } itertools = "0.12" quine-mc_cluskey = "0.2" regex-syntax = "0.8" diff --git a/src/tools/clippy/clippy_lints/src/attrs/inline_always.rs b/src/tools/clippy/clippy_lints/src/attrs/inline_always.rs index 58e51128a0d..b8f93ee5e2c 100644 --- a/src/tools/clippy/clippy_lints/src/attrs/inline_always.rs +++ b/src/tools/clippy/clippy_lints/src/attrs/inline_always.rs @@ -1,10 +1,10 @@ use super::INLINE_ALWAYS; use clippy_utils::diagnostics::span_lint; -use rustc_attr_data_structures::{find_attr, AttributeKind, InlineAttr}; +use rustc_attr_data_structures::{AttributeKind, InlineAttr, find_attr}; use rustc_hir::Attribute; use rustc_lint::LateContext; -use rustc_span::symbol::Symbol; use rustc_span::Span; +use rustc_span::symbol::Symbol; pub(super) fn check(cx: &LateContext<'_>, span: Span, name: Symbol, attrs: &[Attribute]) { if span.from_expansion() { diff --git a/src/tools/clippy/clippy_lints/src/attrs/mod.rs b/src/tools/clippy/clippy_lints/src/attrs/mod.rs index 9a124298041..91c2dc7f3dc 100644 --- a/src/tools/clippy/clippy_lints/src/attrs/mod.rs +++ b/src/tools/clippy/clippy_lints/src/attrs/mod.rs @@ -207,7 +207,7 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does /// Checks for usage of the `#[allow]` attribute and suggests replacing it with - /// the `#[expect]` (See [RFC 2383](https://rust-lang.github.io/rfcs/2383-lint-reasons.html)) + /// the `#[expect]` attribute (See [RFC 2383](https://rust-lang.github.io/rfcs/2383-lint-reasons.html)) /// /// This lint only warns outer attributes (`#[allow]`), as inner attributes /// (`#![allow]`) are usually used to enable or disable lints on a global scale. diff --git a/src/tools/clippy/clippy_lints/src/attrs/utils.rs b/src/tools/clippy/clippy_lints/src/attrs/utils.rs index a5ce2137bff..7b66f91f6c0 100644 --- a/src/tools/clippy/clippy_lints/src/attrs/utils.rs +++ b/src/tools/clippy/clippy_lints/src/attrs/utils.rs @@ -46,11 +46,13 @@ pub(super) fn is_relevant_trait(cx: &LateContext<'_>, item: &TraitItem<'_>) -> b } fn is_relevant_block(cx: &LateContext<'_>, typeck_results: &ty::TypeckResults<'_>, block: &Block<'_>) -> bool { - block.stmts.first().map_or( - block - .expr - .as_ref() - .is_some_and(|e| is_relevant_expr(cx, typeck_results, e)), + block.stmts.first().map_or_else( + || { + block + .expr + .as_ref() + .is_some_and(|e| is_relevant_expr(cx, typeck_results, e)) + }, |stmt| match &stmt.kind { StmtKind::Let(_) => true, StmtKind::Expr(expr) | StmtKind::Semi(expr) => is_relevant_expr(cx, typeck_results, expr), diff --git a/src/tools/clippy/clippy_lints/src/borrow_deref_ref.rs b/src/tools/clippy/clippy_lints/src/borrow_deref_ref.rs index 7cde007a9b6..70c9c45a60c 100644 --- a/src/tools/clippy/clippy_lints/src/borrow_deref_ref.rs +++ b/src/tools/clippy/clippy_lints/src/borrow_deref_ref.rs @@ -2,9 +2,9 @@ use crate::reference::DEREF_ADDROF; use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::source::SpanRangeExt; use clippy_utils::ty::implements_trait; -use clippy_utils::{get_parent_expr, is_from_proc_macro, is_lint_allowed, is_mutable}; +use clippy_utils::{get_parent_expr, is_expr_temporary_value, is_from_proc_macro, is_lint_allowed, is_mutable}; use rustc_errors::Applicability; -use rustc_hir::{BorrowKind, ExprKind, UnOp}; +use rustc_hir::{BorrowKind, Expr, ExprKind, Node, UnOp}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::mir::Mutability; use rustc_middle::ty; @@ -48,7 +48,7 @@ declare_clippy_lint! { declare_lint_pass!(BorrowDerefRef => [BORROW_DEREF_REF]); impl<'tcx> LateLintPass<'tcx> for BorrowDerefRef { - fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &rustc_hir::Expr<'tcx>) { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) { if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, addrof_target) = e.kind && let ExprKind::Unary(UnOp::Deref, deref_target) = addrof_target.kind && !matches!(deref_target.kind, ExprKind::Unary(UnOp::Deref, ..)) @@ -76,6 +76,9 @@ impl<'tcx> LateLintPass<'tcx> for BorrowDerefRef { && let e_ty = cx.typeck_results().expr_ty_adjusted(e) // check if the reference is coercing to a mutable reference && (!matches!(e_ty.kind(), ty::Ref(_, _, Mutability::Mut)) || is_mutable(cx, deref_target)) + // If the new borrow might be itself borrowed mutably and the original reference is not a temporary + // value, do not propose to use it directly. + && (is_expr_temporary_value(cx, deref_target) || !potentially_bound_to_mutable_ref(cx, e)) && let Some(deref_text) = deref_target.span.get_source_text(cx) { span_lint_and_then( @@ -110,3 +113,9 @@ impl<'tcx> LateLintPass<'tcx> for BorrowDerefRef { } } } + +/// Checks if `expr` is used as part of a `let` statement containing a `ref mut` binding. +fn potentially_bound_to_mutable_ref<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool { + matches!(cx.tcx.parent_hir_node(expr.hir_id), Node::LetStmt(let_stmt) + if let_stmt.pat.contains_explicit_ref_binding() == Some(Mutability::Mut)) +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_sign_loss.rs b/src/tools/clippy/clippy_lints/src/casts/cast_sign_loss.rs index 9a1ad8a7473..a70bd886191 100644 --- a/src/tools/clippy/clippy_lints/src/casts/cast_sign_loss.rs +++ b/src/tools/clippy/clippy_lints/src/casts/cast_sign_loss.rs @@ -168,7 +168,7 @@ fn pow_call_result_sign(cx: &LateContext<'_>, base: &Expr<'_>, exponent: &Expr<' // Rust's integer pow() functions take an unsigned exponent. let exponent_val = get_const_unsigned_int_eval(cx, exponent, None); - let exponent_is_even = exponent_val.map(|val| val % 2 == 0); + let exponent_is_even = exponent_val.map(|val| val.is_multiple_of(2)); match (base_sign, exponent_is_even) { // Non-negative bases always return non-negative results, ignoring overflow. diff --git a/src/tools/clippy/clippy_lints/src/collapsible_if.rs b/src/tools/clippy/clippy_lints/src/collapsible_if.rs index 7f6ecea99fb..1854d86c53b 100644 --- a/src/tools/clippy/clippy_lints/src/collapsible_if.rs +++ b/src/tools/clippy/clippy_lints/src/collapsible_if.rs @@ -1,13 +1,16 @@ use clippy_config::Conf; -use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::msrvs::{self, Msrv}; -use clippy_utils::source::{IntoSpan as _, SpanRangeExt, snippet, snippet_block, snippet_block_with_applicability}; +use clippy_utils::source::{IntoSpan as _, SpanRangeExt, snippet, snippet_block_with_applicability}; +use clippy_utils::{span_contains_non_whitespace, tokenize_with_text}; use rustc_ast::BinOpKind; use rustc_errors::Applicability; use rustc_hir::{Block, Expr, ExprKind, Stmt, StmtKind}; +use rustc_lexer::TokenKind; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::impl_lint_pass; -use rustc_span::Span; +use rustc_span::source_map::SourceMap; +use rustc_span::{BytePos, Span}; declare_clippy_lint! { /// ### What it does @@ -90,35 +93,74 @@ impl CollapsibleIf { } } - fn check_collapsible_else_if(cx: &LateContext<'_>, then_span: Span, else_block: &Block<'_>) { - if !block_starts_with_comment(cx, else_block) - && let Some(else_) = expr_block(else_block) + fn check_collapsible_else_if(&self, cx: &LateContext<'_>, then_span: Span, else_block: &Block<'_>) { + if let Some(else_) = expr_block(else_block) && cx.tcx.hir_attrs(else_.hir_id).is_empty() && !else_.span.from_expansion() - && let ExprKind::If(..) = else_.kind + && let ExprKind::If(else_if_cond, ..) = else_.kind + && !block_starts_with_significant_tokens(cx, else_block, else_, self.lint_commented_code) { - // Prevent "elseif" - // Check that the "else" is followed by whitespace - let up_to_else = then_span.between(else_block.span); - let requires_space = if let Some(c) = snippet(cx, up_to_else, "..").chars().last() { - !c.is_whitespace() - } else { - false - }; - - let mut applicability = Applicability::MachineApplicable; - span_lint_and_sugg( + span_lint_and_then( cx, COLLAPSIBLE_ELSE_IF, else_block.span, "this `else { if .. }` block can be collapsed", - "collapse nested if block", - format!( - "{}{}", - if requires_space { " " } else { "" }, - snippet_block_with_applicability(cx, else_.span, "..", Some(else_block.span), &mut applicability) - ), - applicability, + |diag| { + let up_to_else = then_span.between(else_block.span); + let else_before_if = else_.span.shrink_to_lo().with_hi(else_if_cond.span.lo() - BytePos(1)); + if self.lint_commented_code + && let Some(else_keyword_span) = + span_extract_keyword(cx.tcx.sess.source_map(), up_to_else, "else") + && let Some(else_if_keyword_span) = + span_extract_keyword(cx.tcx.sess.source_map(), else_before_if, "if") + { + let else_keyword_span = else_keyword_span.with_leading_whitespace(cx).into_span(); + let else_open_bracket = else_block.span.split_at(1).0.with_leading_whitespace(cx).into_span(); + let else_closing_bracket = { + let end = else_block.span.shrink_to_hi(); + end.with_lo(end.lo() - BytePos(1)) + .with_leading_whitespace(cx) + .into_span() + }; + let sugg = vec![ + // Remove the outer else block `else` + (else_keyword_span, String::new()), + // Replace the inner `if` by `else if` + (else_if_keyword_span, String::from("else if")), + // Remove the outer else block `{` + (else_open_bracket, String::new()), + // Remove the outer else block '}' + (else_closing_bracket, String::new()), + ]; + diag.multipart_suggestion("collapse nested if block", sugg, Applicability::MachineApplicable); + return; + } + + // Prevent "elseif" + // Check that the "else" is followed by whitespace + let requires_space = if let Some(c) = snippet(cx, up_to_else, "..").chars().last() { + !c.is_whitespace() + } else { + false + }; + let mut applicability = Applicability::MachineApplicable; + diag.span_suggestion( + else_block.span, + "collapse nested if block", + format!( + "{}{}", + if requires_space { " " } else { "" }, + snippet_block_with_applicability( + cx, + else_.span, + "..", + Some(else_block.span), + &mut applicability + ) + ), + applicability, + ); + }, ); } } @@ -130,7 +172,7 @@ impl CollapsibleIf { && self.eligible_condition(cx, check_inner) && let ctxt = expr.span.ctxt() && inner.span.ctxt() == ctxt - && (self.lint_commented_code || !block_starts_with_comment(cx, then)) + && !block_starts_with_significant_tokens(cx, then, inner, self.lint_commented_code) { span_lint_and_then( cx, @@ -141,7 +183,7 @@ impl CollapsibleIf { let then_open_bracket = then.span.split_at(1).0.with_leading_whitespace(cx).into_span(); let then_closing_bracket = { let end = then.span.shrink_to_hi(); - end.with_lo(end.lo() - rustc_span::BytePos(1)) + end.with_lo(end.lo() - BytePos(1)) .with_leading_whitespace(cx) .into_span() }; @@ -179,7 +221,7 @@ impl LateLintPass<'_> for CollapsibleIf { if let Some(else_) = else_ && let ExprKind::Block(else_, None) = else_.kind { - Self::check_collapsible_else_if(cx, then.span, else_); + self.check_collapsible_else_if(cx, then.span, else_); } else if else_.is_none() && self.eligible_condition(cx, cond) && let ExprKind::Block(then, None) = then.kind @@ -190,12 +232,16 @@ impl LateLintPass<'_> for CollapsibleIf { } } -fn block_starts_with_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool { - // We trim all opening braces and whitespaces and then check if the next string is a comment. - let trimmed_block_text = snippet_block(cx, block.span, "..", None) - .trim_start_matches(|c: char| c.is_whitespace() || c == '{') - .to_owned(); - trimmed_block_text.starts_with("//") || trimmed_block_text.starts_with("/*") +// Check that nothing significant can be found but whitespaces between the initial `{` of `block` +// and the beginning of `stop_at`. +fn block_starts_with_significant_tokens( + cx: &LateContext<'_>, + block: &Block<'_>, + stop_at: &Expr<'_>, + lint_commented_code: bool, +) -> bool { + let span = block.span.split_at(1).1.until(stop_at.span); + span_contains_non_whitespace(cx, span, lint_commented_code) } /// If `block` is a block with either one expression or a statement containing an expression, @@ -226,3 +272,16 @@ fn parens_around(expr: &Expr<'_>) -> Vec<(Span, String)> { vec![] } } + +fn span_extract_keyword(sm: &SourceMap, span: Span, keyword: &str) -> Option<Span> { + let snippet = sm.span_to_snippet(span).ok()?; + tokenize_with_text(&snippet) + .filter(|(t, s, _)| matches!(t, TokenKind::Ident if *s == keyword)) + .map(|(_, _, inner)| { + span.split_at(u32::try_from(inner.start).unwrap()) + .1 + .split_at(u32::try_from(inner.end - inner.start).unwrap()) + .0 + }) + .next() +} diff --git a/src/tools/clippy/clippy_lints/src/copies.rs b/src/tools/clippy/clippy_lints/src/copies.rs index 5ef726638a5..27918698cd6 100644 --- a/src/tools/clippy/clippy_lints/src/copies.rs +++ b/src/tools/clippy/clippy_lints/src/copies.rs @@ -11,7 +11,7 @@ use clippy_utils::{ use core::iter; use core::ops::ControlFlow; use rustc_errors::Applicability; -use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, HirIdSet, Stmt, StmtKind, intravisit}; +use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, HirIdSet, LetStmt, Node, Stmt, StmtKind, intravisit}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::TyCtxt; use rustc_session::impl_lint_pass; @@ -295,7 +295,7 @@ fn lint_branches_sharing_code<'tcx>( sugg, Applicability::Unspecified, ); - if !cx.typeck_results().expr_ty(expr).is_unit() { + if is_expr_parent_assignment(cx, expr) || !cx.typeck_results().expr_ty(expr).is_unit() { diag.note("the end suggestion probably needs some adjustments to use the expression result correctly"); } } @@ -660,3 +660,17 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) { ); } } + +fn is_expr_parent_assignment(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let parent = cx.tcx.parent_hir_node(expr.hir_id); + if let Node::LetStmt(LetStmt { init: Some(e), .. }) + | Node::Expr(Expr { + kind: ExprKind::Assign(_, e, _), + .. + }) = parent + { + return e.hir_id == expr.hir_id; + } + + false +} diff --git a/src/tools/clippy/clippy_lints/src/declare_clippy_lint.rs b/src/tools/clippy/clippy_lints/src/declare_clippy_lint.rs deleted file mode 100644 index 9f82f876727..00000000000 --- a/src/tools/clippy/clippy_lints/src/declare_clippy_lint.rs +++ /dev/null @@ -1,168 +0,0 @@ -#[macro_export] -#[allow(clippy::crate_in_macro_def)] -macro_rules! declare_clippy_lint { - (@ - $(#[doc = $lit:literal])* - pub $lint_name:ident, - $level:ident, - $lintcategory:expr, - $desc:literal, - $version_expr:expr, - $version_lit:literal - $(, $eval_always: literal)? - ) => { - rustc_session::declare_tool_lint! { - $(#[doc = $lit])* - #[clippy::version = $version_lit] - pub clippy::$lint_name, - $level, - $desc, - report_in_external_macro:true - $(, @eval_always = $eval_always)? - } - - pub(crate) static ${concat($lint_name, _INFO)}: &'static crate::LintInfo = &crate::LintInfo { - lint: &$lint_name, - category: $lintcategory, - explanation: concat!($($lit,"\n",)*), - location: concat!(file!(), "#L", line!()), - version: $version_expr - }; - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - restriction, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Allow, crate::LintCategory::Restriction, $desc, - Some($version), $version - $(, $eval_always)? - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - style, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Warn, crate::LintCategory::Style, $desc, - Some($version), $version - $(, $eval_always)? - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - correctness, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Deny, crate::LintCategory::Correctness, $desc, - Some($version), $version - $(, $eval_always)? - - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - perf, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Warn, crate::LintCategory::Perf, $desc, - Some($version), $version - $(, $eval_always)? - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - complexity, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Warn, crate::LintCategory::Complexity, $desc, - Some($version), $version - $(, $eval_always)? - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - suspicious, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Warn, crate::LintCategory::Suspicious, $desc, - Some($version), $version - $(, $eval_always)? - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - nursery, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Allow, crate::LintCategory::Nursery, $desc, - Some($version), $version - $(, $eval_always)? - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - pedantic, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Allow, crate::LintCategory::Pedantic, $desc, - Some($version), $version - $(, $eval_always)? - } - }; - ( - $(#[doc = $lit:literal])* - #[clippy::version = $version:literal] - pub $lint_name:ident, - cargo, - $desc:literal - $(, @eval_always = $eval_always: literal)? - ) => { - declare_clippy_lint! {@ - $(#[doc = $lit])* - pub $lint_name, Allow, crate::LintCategory::Cargo, $desc, - Some($version), $version - $(, $eval_always)? - } - }; -} diff --git a/src/tools/clippy/clippy_lints/src/declared_lints.rs b/src/tools/clippy/clippy_lints/src/declared_lints.rs index 1e3907d9ede..c3f8e02b4c0 100644 --- a/src/tools/clippy/clippy_lints/src/declared_lints.rs +++ b/src/tools/clippy/clippy_lints/src/declared_lints.rs @@ -2,7 +2,7 @@ // Use that command to update this file and do not edit by hand. // Manual edits will be overwritten. -pub static LINTS: &[&crate::LintInfo] = &[ +pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::absolute_paths::ABSOLUTE_PATHS_INFO, crate::almost_complete_range::ALMOST_COMPLETE_RANGE_INFO, crate::approx_const::APPROX_CONSTANT_INFO, @@ -112,6 +112,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::disallowed_names::DISALLOWED_NAMES_INFO, crate::disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS_INFO, crate::disallowed_types::DISALLOWED_TYPES_INFO, + crate::doc::DOC_BROKEN_LINK_INFO, crate::doc::DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS_INFO, crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO, crate::doc::DOC_LAZY_CONTINUATION_INFO, @@ -590,6 +591,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::operators::IMPOSSIBLE_COMPARISONS_INFO, crate::operators::INEFFECTIVE_BIT_MASK_INFO, crate::operators::INTEGER_DIVISION_INFO, + crate::operators::MANUAL_IS_MULTIPLE_OF_INFO, crate::operators::MANUAL_MIDPOINT_INFO, crate::operators::MISREFACTORED_ASSIGN_OP_INFO, crate::operators::MODULO_ARITHMETIC_INFO, diff --git a/src/tools/clippy/clippy_lints/src/disallowed_macros.rs b/src/tools/clippy/clippy_lints/src/disallowed_macros.rs index 9814d4fa84f..d55aeae98ed 100644 --- a/src/tools/clippy/clippy_lints/src/disallowed_macros.rs +++ b/src/tools/clippy/clippy_lints/src/disallowed_macros.rs @@ -40,6 +40,9 @@ declare_clippy_lint! { /// # When using an inline table, can add a `reason` for why the macro /// # is disallowed. /// { path = "serde::Serialize", reason = "no serializing" }, + /// # This would normally error if the path is incorrect, but with `allow-invalid` = `true`, + /// # it will be silently ignored + /// { path = "std::invalid_macro", reason = "use alternative instead", allow-invalid = true } /// ] /// ``` /// ```no_run diff --git a/src/tools/clippy/clippy_lints/src/disallowed_methods.rs b/src/tools/clippy/clippy_lints/src/disallowed_methods.rs index fb970e17f38..8c067432cb4 100644 --- a/src/tools/clippy/clippy_lints/src/disallowed_methods.rs +++ b/src/tools/clippy/clippy_lints/src/disallowed_methods.rs @@ -34,6 +34,9 @@ declare_clippy_lint! { /// { path = "std::vec::Vec::leak", reason = "no leaking memory" }, /// # Can also add a `replacement` that will be offered as a suggestion. /// { path = "std::sync::Mutex::new", reason = "prefer faster & simpler non-poisonable mutex", replacement = "parking_lot::Mutex::new" }, + /// # This would normally error if the path is incorrect, but with `allow-invalid` = `true`, + /// # it will be silently ignored + /// { path = "std::fs::InvalidPath", reason = "use alternative instead", allow-invalid = true }, /// ] /// ``` /// diff --git a/src/tools/clippy/clippy_lints/src/disallowed_types.rs b/src/tools/clippy/clippy_lints/src/disallowed_types.rs index 7875cdd77e8..9a82327a0d7 100644 --- a/src/tools/clippy/clippy_lints/src/disallowed_types.rs +++ b/src/tools/clippy/clippy_lints/src/disallowed_types.rs @@ -35,6 +35,9 @@ declare_clippy_lint! { /// { path = "std::net::Ipv4Addr", reason = "no IPv4 allowed" }, /// # Can also add a `replacement` that will be offered as a suggestion. /// { path = "std::sync::Mutex", reason = "prefer faster & simpler non-poisonable mutex", replacement = "parking_lot::Mutex" }, + /// # This would normally error if the path is incorrect, but with `allow-invalid` = `true`, + /// # it will be silently ignored + /// { path = "std::invalid::Type", reason = "use alternative instead", allow-invalid = true } /// ] /// ``` /// diff --git a/src/tools/clippy/clippy_lints/src/doc/broken_link.rs b/src/tools/clippy/clippy_lints/src/doc/broken_link.rs new file mode 100644 index 00000000000..4af10510023 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/doc/broken_link.rs @@ -0,0 +1,83 @@ +use clippy_utils::diagnostics::span_lint; +use pulldown_cmark::BrokenLink as PullDownBrokenLink; +use rustc_lint::LateContext; +use rustc_resolve::rustdoc::{DocFragment, source_span_for_markdown_range}; +use rustc_span::{BytePos, Pos, Span}; + +use super::DOC_BROKEN_LINK; + +/// Scan and report broken link on documents. +/// It ignores false positives detected by `pulldown_cmark`, and only +/// warns users when the broken link is consider a URL. +// NOTE: We don't check these other cases because +// rustdoc itself will check and warn about it: +// - When a link url is broken across multiple lines in the URL path part +// - When a link tag is missing the close parenthesis character at the end. +// - When a link has whitespace within the url link. +pub fn check(cx: &LateContext<'_>, bl: &PullDownBrokenLink<'_>, doc: &str, fragments: &[DocFragment]) { + warn_if_broken_link(cx, bl, doc, fragments); +} + +fn warn_if_broken_link(cx: &LateContext<'_>, bl: &PullDownBrokenLink<'_>, doc: &str, fragments: &[DocFragment]) { + if let Some((span, _)) = source_span_for_markdown_range(cx.tcx, doc, &bl.span, fragments) { + let mut len = 0; + + // grab raw link data + let (_, raw_link) = doc.split_at(bl.span.start); + + // strip off link text part + let raw_link = match raw_link.split_once(']') { + None => return, + Some((prefix, suffix)) => { + len += prefix.len() + 1; + suffix + }, + }; + + let raw_link = match raw_link.split_once('(') { + None => return, + Some((prefix, suffix)) => { + if !prefix.is_empty() { + // there is text between ']' and '(' chars, so it is not a valid link + return; + } + len += prefix.len() + 1; + suffix + }, + }; + + if raw_link.starts_with("(http") { + // reduce chances of false positive reports + // by limiting this checking only to http/https links. + return; + } + + for c in raw_link.chars() { + if c == ')' { + // it is a valid link + return; + } + + if c == '\n' { + report_broken_link(cx, span, len); + break; + } + + len += 1; + } + } +} + +fn report_broken_link(cx: &LateContext<'_>, frag_span: Span, offset: usize) { + let start = frag_span.lo(); + let end = start + BytePos::from_usize(offset); + + let span = Span::new(start, end, frag_span.ctxt(), frag_span.parent()); + + span_lint( + cx, + DOC_BROKEN_LINK, + span, + "possible broken doc link: broken across multiple lines", + ); +} diff --git a/src/tools/clippy/clippy_lints/src/doc/doc_suspicious_footnotes.rs b/src/tools/clippy/clippy_lints/src/doc/doc_suspicious_footnotes.rs index d3c39686976..3330cc5defd 100644 --- a/src/tools/clippy/clippy_lints/src/doc/doc_suspicious_footnotes.rs +++ b/src/tools/clippy/clippy_lints/src/doc/doc_suspicious_footnotes.rs @@ -49,11 +49,7 @@ pub fn check(cx: &LateContext<'_>, doc: &str, range: Range<usize>, fragments: &F .filter(|attr| attr.span().overlaps(this_fragment.span)) .rev() .find_map(|attr| { - Some(( - attr, - attr.doc_str_and_comment_kind()?, - attr.doc_resolution_scope()?, - )) + Some((attr, attr.doc_str_and_comment_kind()?, attr.doc_resolution_scope()?)) }) .unwrap(); let (to_add, terminator) = match (doc_attr_comment_kind, attr_style) { diff --git a/src/tools/clippy/clippy_lints/src/doc/mod.rs b/src/tools/clippy/clippy_lints/src/doc/mod.rs index e0fc2fd9347..5ea55e102df 100644 --- a/src/tools/clippy/clippy_lints/src/doc/mod.rs +++ b/src/tools/clippy/clippy_lints/src/doc/mod.rs @@ -24,6 +24,7 @@ use rustc_span::edition::Edition; use std::ops::Range; use url::Url; +mod broken_link; mod doc_comment_double_space_linebreaks; mod doc_suspicious_footnotes; mod include_in_doc_without_cfg; @@ -294,6 +295,34 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does + /// Checks the doc comments have unbroken links, mostly caused + /// by bad formatted links such as broken across multiple lines. + /// + /// ### Why is this bad? + /// Because documentation generated by rustdoc will be broken + /// since expected links won't be links and just text. + /// + /// ### Examples + /// This link is broken: + /// ```no_run + /// /// [example of a bad link](https:// + /// /// github.com/rust-lang/rust-clippy/) + /// pub fn do_something() {} + /// ``` + /// + /// It shouldn't be broken across multiple lines to work: + /// ```no_run + /// /// [example of a good link](https://github.com/rust-lang/rust-clippy/) + /// pub fn do_something() {} + /// ``` + #[clippy::version = "1.84.0"] + pub DOC_BROKEN_LINK, + pedantic, + "broken document link" +} + +declare_clippy_lint! { + /// ### What it does /// Checks for the doc comments of publicly visible /// safe functions and traits and warns if there is a `# Safety` section. /// @@ -656,6 +685,7 @@ impl Documentation { impl_lint_pass!(Documentation => [ DOC_LINK_CODE, DOC_LINK_WITH_QUOTES, + DOC_BROKEN_LINK, DOC_MARKDOWN, DOC_NESTED_REFDEFS, MISSING_SAFETY_DOC, @@ -765,8 +795,8 @@ impl Fragments<'_> { /// get the span for the markdown range. Note that this function is not cheap, use it with /// caution. #[must_use] - fn span(&self, cx: &LateContext<'_>, range: Range<usize>) -> Option<Span> { - source_span_for_markdown_range(cx.tcx, self.doc, &range, self.fragments) + fn span(self, cx: &LateContext<'_>, range: Range<usize>) -> Option<Span> { + source_span_for_markdown_range(cx.tcx, self.doc, &range, self.fragments).map(|(sp, _)| sp) } } @@ -786,9 +816,9 @@ struct DocHeaders { /// back in the various late lint pass methods if they need the final doc headers, like "Safety" or /// "Panics" sections. fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[Attribute]) -> Option<DocHeaders> { - /// We don't want the parser to choke on intra doc links. Since we don't - /// actually care about rendering them, just pretend that all broken links - /// point to a fake address. + // We don't want the parser to choke on intra doc links. Since we don't + // actually care about rendering them, just pretend that all broken links + // point to a fake address. #[expect(clippy::unnecessary_wraps)] // we're following a type signature fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> { Some(("fake".into(), "fake".into())) @@ -828,14 +858,12 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[ return Some(DocHeaders::default()); } - let mut cb = fake_broken_link_callback; - check_for_code_clusters( cx, pulldown_cmark::Parser::new_with_broken_link_callback( &doc, main_body_opts() - Options::ENABLE_SMART_PUNCTUATION, - Some(&mut cb), + Some(&mut fake_broken_link_callback), ) .into_offset_iter(), &doc, @@ -845,9 +873,17 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[ }, ); + // NOTE: check_doc uses it own cb function, + // to avoid causing duplicated diagnostics for the broken link checker. + let mut full_fake_broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> { + broken_link::check(cx, &bl, &doc, &fragments); + Some(("fake".into(), "fake".into())) + }; + // disable smart punctuation to pick up ['link'] more easily let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION; - let parser = pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut cb)); + let parser = + pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut full_fake_broken_link_callback)); Some(check_doc( cx, diff --git a/src/tools/clippy/clippy_lints/src/doc/needless_doctest_main.rs b/src/tools/clippy/clippy_lints/src/doc/needless_doctest_main.rs index 7ba11c20f45..74283d7ba86 100644 --- a/src/tools/clippy/clippy_lints/src/doc/needless_doctest_main.rs +++ b/src/tools/clippy/clippy_lints/src/doc/needless_doctest_main.rs @@ -71,6 +71,7 @@ pub fn check( 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, @@ -89,9 +90,14 @@ pub fn check( // Another function was found; this case is ignored for needless_doctest_main ItemKind::Fn(fn_) => { eligible = false; - if !ignore { - get_test_spans(&item, fn_.ident, &mut test_attr_spans); + 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(..) @@ -104,7 +110,10 @@ pub fn check( }, Ok(None) => break, Err(e) => { - e.cancel(); + // 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); }, } @@ -119,6 +128,18 @@ pub fn check( 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]")) { + 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(); diff --git a/src/tools/clippy/clippy_lints/src/empty_line_after.rs b/src/tools/clippy/clippy_lints/src/empty_line_after.rs index 0c5f8bbf4ca..3bd74856165 100644 --- a/src/tools/clippy/clippy_lints/src/empty_line_after.rs +++ b/src/tools/clippy/clippy_lints/src/empty_line_after.rs @@ -10,7 +10,7 @@ use rustc_errors::{Applicability, Diag, SuggestionStyle}; use rustc_lexer::TokenKind; use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; use rustc_session::impl_lint_pass; -use rustc_span::{BytePos, ExpnKind, Ident, InnerSpan, Span, SpanData, Symbol, kw}; +use rustc_span::{BytePos, ExpnKind, Ident, InnerSpan, Span, SpanData, Symbol, kw, sym}; declare_clippy_lint! { /// ### What it does @@ -129,10 +129,55 @@ struct Stop { kind: StopKind, first: usize, last: usize, + name: Option<Symbol>, } impl Stop { - fn convert_to_inner(&self) -> (Span, String) { + fn is_outer_attr_only(&self) -> bool { + let Some(name) = self.name else { + return false; + }; + // Check if the attribute only has effect when as an outer attribute + // The below attributes are collected from the builtin attributes of The Rust Reference + // https://doc.rust-lang.org/reference/attributes.html#r-attributes.builtin + // And the comments below are from compiler errors and warnings + matches!( + name, + // Cannot be used at crate level + sym::repr | sym::test | sym::derive | sym::automatically_derived | sym::path | sym::global_allocator | + // Only has an effect on macro definitions + sym::macro_export | + // Only be applied to trait definitions + sym::on_unimplemented | + // Only be placed on trait implementations + sym::do_not_recommend | + // Only has an effect on items + sym::ignore | sym::should_panic | sym::proc_macro | sym::proc_macro_derive | sym::proc_macro_attribute | + // Has no effect when applied to a module + sym::must_use | + // Should be applied to a foreign function or static + sym::link_name | sym::link_ordinal | sym::link_section | + // Should be applied to an `extern crate` item + sym::no_link | + // Should be applied to a free function, impl method or static + sym::export_name | sym::no_mangle | + // Should be applied to a `static` variable + sym::used | + // Should be applied to function or closure + sym::inline | + // Should be applied to a function definition + sym::cold | sym::target_feature | sym::track_caller | sym::instruction_set | + // Should be applied to a struct or enum + sym::non_exhaustive | + // Note: No any warning when it as an inner attribute, but it has no effect + sym::panic_handler + ) + } + + fn convert_to_inner(&self) -> Option<(Span, String)> { + if self.is_outer_attr_only() { + return None; + } let inner = match self.kind { // #![...] StopKind::Attr => InnerSpan::new(1, 1), @@ -140,7 +185,7 @@ impl Stop { // ^ ^ StopKind::Doc(_) => InnerSpan::new(2, 3), }; - (self.span.from_inner(inner), "!".into()) + Some((self.span.from_inner(inner), "!".into())) } fn comment_out(&self, cx: &EarlyContext<'_>, suggestions: &mut Vec<(Span, String)>) { @@ -177,6 +222,7 @@ impl Stop { }, first: file.lookup_line(file.relative_position(lo))?, last: file.lookup_line(file.relative_position(hi))?, + name: attr.name(), }) } } @@ -356,6 +402,12 @@ impl EmptyLineAfter { if let Some(parent) = self.items.iter().rev().nth(1) && (parent.kind == "module" || parent.kind == "crate") && parent.mod_items == Some(id) + && let suggestions = gaps + .iter() + .flat_map(|gap| gap.prev_chunk) + .filter_map(Stop::convert_to_inner) + .collect::<Vec<_>>() + && !suggestions.is_empty() { let desc = if parent.kind == "module" { "parent module" @@ -367,10 +419,7 @@ impl EmptyLineAfter { StopKind::Attr => format!("if the attribute should apply to the {desc} use an inner attribute"), StopKind::Doc(_) => format!("if the comment should document the {desc} use an inner doc comment"), }, - gaps.iter() - .flat_map(|gap| gap.prev_chunk) - .map(Stop::convert_to_inner) - .collect(), + suggestions, Applicability::MaybeIncorrect, ); } @@ -425,6 +474,7 @@ impl EmptyLineAfter { first: line.line, // last doesn't need to be accurate here, we don't compare it with anything last: line.line, + name: None, }); } diff --git a/src/tools/clippy/clippy_lints/src/eta_reduction.rs b/src/tools/clippy/clippy_lints/src/eta_reduction.rs index 6ed7c87915b..0288747d6f3 100644 --- a/src/tools/clippy/clippy_lints/src/eta_reduction.rs +++ b/src/tools/clippy/clippy_lints/src/eta_reduction.rs @@ -1,4 +1,4 @@ -use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::diagnostics::span_lint_hir_and_then; use clippy_utils::higher::VecArgs; use clippy_utils::source::{snippet_opt, snippet_with_applicability}; use clippy_utils::ty::get_type_diagnostic_name; @@ -7,6 +7,7 @@ use clippy_utils::{ get_path_from_caller_to_method_type, is_adjusted, is_no_std_crate, path_to_local, path_to_local_id, }; use rustc_abi::ExternAbi; +use rustc_attr_data_structures::{AttributeKind, find_attr}; use rustc_errors::Applicability; use rustc_hir::{BindingMode, Expr, ExprKind, FnRetTy, GenericArgs, Param, PatKind, QPath, Safety, TyKind}; use rustc_infer::infer::TyCtxtInferExt; @@ -108,14 +109,20 @@ fn check_closure<'tcx>(cx: &LateContext<'tcx>, outer_receiver: Option<&Expr<'tcx { let vec_crate = if is_no_std_crate(cx) { "alloc" } else { "std" }; // replace `|| vec![]` with `Vec::new` - span_lint_and_sugg( + span_lint_hir_and_then( cx, REDUNDANT_CLOSURE, + expr.hir_id, expr.span, "redundant closure", - "replace the closure with `Vec::new`", - format!("{vec_crate}::vec::Vec::new"), - Applicability::MachineApplicable, + |diag| { + diag.span_suggestion( + expr.span, + "replace the closure with `Vec::new`", + format!("{vec_crate}::vec::Vec::new"), + Applicability::MachineApplicable, + ); + }, ); } // skip `foo(|| macro!())` @@ -155,7 +162,7 @@ fn check_closure<'tcx>(cx: &LateContext<'tcx>, outer_receiver: Option<&Expr<'tcx let sig = match callee_ty_adjusted.kind() { ty::FnDef(def, _) => { // Rewriting `x(|| f())` to `x(f)` where f is marked `#[track_caller]` moves the `Location` - if cx.tcx.has_attr(*def, sym::track_caller) { + if find_attr!(cx.tcx.get_all_attrs(*def), AttributeKind::TrackCaller(..)) { return; } @@ -197,46 +204,53 @@ fn check_closure<'tcx>(cx: &LateContext<'tcx>, outer_receiver: Option<&Expr<'tcx // For now ignore all callee types which reference a type parameter. && !generic_args.types().any(|t| matches!(t.kind(), ty::Param(_))) { - span_lint_and_then(cx, REDUNDANT_CLOSURE, expr.span, "redundant closure", |diag| { - if let Some(mut snippet) = snippet_opt(cx, callee.span) { - if path_to_local(callee).is_some_and(|l| { - // FIXME: Do we really need this `local_used_in` check? - // Isn't it checking something like... `callee(callee)`? - // If somehow this check is needed, add some test for it, - // 'cuz currently nothing changes after deleting this check. - local_used_in(cx, l, args) || local_used_after_expr(cx, l, expr) - }) { - match cx - .tcx - .infer_ctxt() - .build(cx.typing_mode()) - .err_ctxt() - .type_implements_fn_trait( - cx.param_env, - Binder::bind_with_vars(callee_ty_adjusted, List::empty()), - ty::PredicatePolarity::Positive, - ) { - // Mutable closure is used after current expr; we cannot consume it. - Ok((ClosureKind::FnMut, _)) => snippet = format!("&mut {snippet}"), - Ok((ClosureKind::Fn, _)) if !callee_ty_raw.is_ref() => { - snippet = format!("&{snippet}"); - }, - _ => (), + span_lint_hir_and_then( + cx, + REDUNDANT_CLOSURE, + expr.hir_id, + expr.span, + "redundant closure", + |diag| { + if let Some(mut snippet) = snippet_opt(cx, callee.span) { + if path_to_local(callee).is_some_and(|l| { + // FIXME: Do we really need this `local_used_in` check? + // Isn't it checking something like... `callee(callee)`? + // If somehow this check is needed, add some test for it, + // 'cuz currently nothing changes after deleting this check. + local_used_in(cx, l, args) || local_used_after_expr(cx, l, expr) + }) { + match cx + .tcx + .infer_ctxt() + .build(cx.typing_mode()) + .err_ctxt() + .type_implements_fn_trait( + cx.param_env, + Binder::bind_with_vars(callee_ty_adjusted, List::empty()), + ty::PredicatePolarity::Positive, + ) { + // Mutable closure is used after current expr; we cannot consume it. + Ok((ClosureKind::FnMut, _)) => snippet = format!("&mut {snippet}"), + Ok((ClosureKind::Fn, _)) if !callee_ty_raw.is_ref() => { + snippet = format!("&{snippet}"); + }, + _ => (), + } } + diag.span_suggestion( + expr.span, + "replace the closure with the function itself", + snippet, + Applicability::MachineApplicable, + ); } - diag.span_suggestion( - expr.span, - "replace the closure with the function itself", - snippet, - Applicability::MachineApplicable, - ); - } - }); + }, + ); } }, ExprKind::MethodCall(path, self_, args, _) if check_inputs(typeck, body.params, Some(self_), args) => { if let Some(method_def_id) = typeck.type_dependent_def_id(body.value.hir_id) - && !cx.tcx.has_attr(method_def_id, sym::track_caller) + && !find_attr!(cx.tcx.get_all_attrs(method_def_id), AttributeKind::TrackCaller(..)) && check_sig(closure_sig, cx.tcx.fn_sig(method_def_id).skip_binder().skip_binder()) { let mut app = Applicability::MachineApplicable; @@ -244,9 +258,10 @@ fn check_closure<'tcx>(cx: &LateContext<'tcx>, outer_receiver: Option<&Expr<'tcx Some(span) => format!("::{}", snippet_with_applicability(cx, span, "<..>", &mut app)), None => String::new(), }; - span_lint_and_then( + span_lint_hir_and_then( cx, REDUNDANT_CLOSURE_FOR_METHOD_CALLS, + expr.hir_id, expr.span, "redundant closure", |diag| { diff --git a/src/tools/clippy/clippy_lints/src/exhaustive_items.rs b/src/tools/clippy/clippy_lints/src/exhaustive_items.rs index 1fb0e4d24d0..86d9038ec45 100644 --- a/src/tools/clippy/clippy_lints/src/exhaustive_items.rs +++ b/src/tools/clippy/clippy_lints/src/exhaustive_items.rs @@ -76,7 +76,7 @@ impl LateLintPass<'_> for ExhaustiveItems { "exported enums should not be exhaustive", [].as_slice(), ), - ItemKind::Struct(_, _, v) => ( + ItemKind::Struct(_, _, v) if v.fields().iter().all(|f| f.default.is_none()) => ( EXHAUSTIVE_STRUCTS, "exported structs should not be exhaustive", v.fields(), diff --git a/src/tools/clippy/clippy_lints/src/floating_point_arithmetic.rs b/src/tools/clippy/clippy_lints/src/floating_point_arithmetic.rs index 3c7e83b0697..b3c9e860758 100644 --- a/src/tools/clippy/clippy_lints/src/floating_point_arithmetic.rs +++ b/src/tools/clippy/clippy_lints/src/floating_point_arithmetic.rs @@ -5,14 +5,13 @@ use clippy_utils::{ eq_expr_value, get_parent_expr, higher, is_in_const_context, is_inherent_method_call, is_no_std_crate, numeric_literal, peel_blocks, sugg, sym, }; +use rustc_ast::ast; use rustc_errors::Applicability; use rustc_hir::{BinOpKind, Expr, ExprKind, PathSegment, UnOp}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty; use rustc_session::declare_lint_pass; use rustc_span::source_map::Spanned; - -use rustc_ast::ast; use std::f32::consts as f32_consts; use std::f64::consts as f64_consts; use sugg::Sugg; diff --git a/src/tools/clippy/clippy_lints/src/functions/must_use.rs b/src/tools/clippy/clippy_lints/src/functions/must_use.rs index c0c23e217fd..d959981a83c 100644 --- a/src/tools/clippy/clippy_lints/src/functions/must_use.rs +++ b/src/tools/clippy/clippy_lints/src/functions/must_use.rs @@ -14,9 +14,9 @@ use clippy_utils::source::SpanRangeExt; use clippy_utils::ty::is_must_use_ty; use clippy_utils::visitors::for_each_expr_without_closures; use clippy_utils::{return_ty, trait_ref_of_method}; -use rustc_trait_selection::error_reporting::InferCtxtErrorExt; -use rustc_span::Symbol; use rustc_attr_data_structures::{AttributeKind, find_attr}; +use rustc_span::Symbol; +use rustc_trait_selection::error_reporting::InferCtxtErrorExt; use core::ops::ControlFlow; @@ -34,7 +34,17 @@ pub(super) fn check_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_> let is_public = cx.effective_visibilities.is_exported(item.owner_id.def_id); let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); if let Some((attr_span, reason)) = attr { - check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, *attr_span, *reason, attrs, sig); + check_needless_must_use( + cx, + sig.decl, + item.owner_id, + item.span, + fn_header_span, + *attr_span, + *reason, + attrs, + sig, + ); } else if is_public && !is_proc_macro(attrs) && !find_attr!(attrs, AttributeKind::NoMangle(..)) { check_must_use_candidate( cx, @@ -54,9 +64,20 @@ pub(super) fn check_impl_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::Imp let is_public = cx.effective_visibilities.is_exported(item.owner_id.def_id); let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); let attrs = cx.tcx.hir_attrs(item.hir_id()); - let attr = find_attr!(cx.tcx.hir_attrs(item.hir_id()), AttributeKind::MustUse { span, reason } => (span, reason)); + let attr = + find_attr!(cx.tcx.hir_attrs(item.hir_id()), AttributeKind::MustUse { span, reason } => (span, reason)); if let Some((attr_span, reason)) = attr { - check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, *attr_span, *reason, attrs, sig); + check_needless_must_use( + cx, + sig.decl, + item.owner_id, + item.span, + fn_header_span, + *attr_span, + *reason, + attrs, + sig, + ); } else if is_public && !is_proc_macro(attrs) && trait_ref_of_method(cx, item.owner_id).is_none() { check_must_use_candidate( cx, @@ -77,9 +98,20 @@ pub(super) fn check_trait_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::Tr let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); let attrs = cx.tcx.hir_attrs(item.hir_id()); - let attr = find_attr!(cx.tcx.hir_attrs(item.hir_id()), AttributeKind::MustUse { span, reason } => (span, reason)); + let attr = + find_attr!(cx.tcx.hir_attrs(item.hir_id()), AttributeKind::MustUse { span, reason } => (span, reason)); if let Some((attr_span, reason)) = attr { - check_needless_must_use(cx, sig.decl, item.owner_id, item.span, fn_header_span, *attr_span, *reason, attrs, sig); + check_needless_must_use( + cx, + sig.decl, + item.owner_id, + item.span, + fn_header_span, + *attr_span, + *reason, + attrs, + sig, + ); } else if let hir::TraitFn::Provided(eid) = *eid { let body = cx.tcx.hir_body(eid); if attr.is_none() && is_public && !is_proc_macro(attrs) { @@ -121,12 +153,7 @@ fn check_needless_must_use( fn_header_span, "this unit-returning function has a `#[must_use]` attribute", |diag| { - diag.span_suggestion( - attr_span, - "remove the attribute", - "", - Applicability::MachineApplicable, - ); + diag.span_suggestion(attr_span, "remove the attribute", "", Applicability::MachineApplicable); }, ); } else { diff --git a/src/tools/clippy/clippy_lints/src/if_not_else.rs b/src/tools/clippy/clippy_lints/src/if_not_else.rs index 45f9aa0a53e..ab7a965b367 100644 --- a/src/tools/clippy/clippy_lints/src/if_not_else.rs +++ b/src/tools/clippy/clippy_lints/src/if_not_else.rs @@ -1,4 +1,4 @@ -use clippy_utils::consts::{ConstEvalCtxt, Constant}; +use clippy_utils::consts::is_zero_integer_const; use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; use clippy_utils::is_else_clause; use clippy_utils::source::{HasSession, indent_of, reindent_multiline, snippet}; @@ -48,13 +48,6 @@ declare_clippy_lint! { declare_lint_pass!(IfNotElse => [IF_NOT_ELSE]); -fn is_zero_const(expr: &Expr<'_>, cx: &LateContext<'_>) -> bool { - if let Some(value) = ConstEvalCtxt::new(cx).eval_simple(expr) { - return Constant::Int(0) == value; - } - false -} - impl LateLintPass<'_> for IfNotElse { fn check_expr(&mut self, cx: &LateContext<'_>, e: &Expr<'_>) { if let ExprKind::If(cond, cond_inner, Some(els)) = e.kind @@ -68,7 +61,7 @@ impl LateLintPass<'_> for IfNotElse { ), // Don't lint on `… != 0`, as these are likely to be bit tests. // For example, `if foo & 0x0F00 != 0 { … } else { … }` is already in the "proper" order. - ExprKind::Binary(op, _, rhs) if op.node == BinOpKind::Ne && !is_zero_const(rhs, cx) => ( + ExprKind::Binary(op, _, rhs) if op.node == BinOpKind::Ne && !is_zero_integer_const(cx, rhs) => ( "unnecessary `!=` operation", "change to `==` and swap the blocks of the `if`/`else`", ), diff --git a/src/tools/clippy/clippy_lints/src/inline_fn_without_body.rs b/src/tools/clippy/clippy_lints/src/inline_fn_without_body.rs index 617c006795b..ffe6ad14f63 100644 --- a/src/tools/clippy/clippy_lints/src/inline_fn_without_body.rs +++ b/src/tools/clippy/clippy_lints/src/inline_fn_without_body.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::sugg::DiagExt; -use rustc_attr_data_structures::{find_attr, AttributeKind}; +use rustc_attr_data_structures::{AttributeKind, find_attr}; use rustc_errors::Applicability; use rustc_hir::{TraitFn, TraitItem, TraitItemKind}; use rustc_lint::{LateContext, LateLintPass}; @@ -33,10 +33,10 @@ impl<'tcx> LateLintPass<'tcx> for InlineFnWithoutBody { fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) { if let TraitItemKind::Fn(_, TraitFn::Required(_)) = item.kind && let Some(attr_span) = find_attr!(cx - .tcx - .hir_attrs(item.hir_id()), - AttributeKind::Inline(_, span) => *span - ) + .tcx + .hir_attrs(item.hir_id()), + AttributeKind::Inline(_, span) => *span + ) { span_lint_and_then( cx, diff --git a/src/tools/clippy/clippy_lints/src/lib.rs b/src/tools/clippy/clippy_lints/src/lib.rs index be9142b17fe..96a6dee5885 100644 --- a/src/tools/clippy/clippy_lints/src/lib.rs +++ b/src/tools/clippy/clippy_lints/src/lib.rs @@ -59,10 +59,10 @@ extern crate smallvec; extern crate thin_vec; #[macro_use] -mod declare_clippy_lint; +extern crate clippy_utils; #[macro_use] -extern crate clippy_utils; +extern crate declare_clippy_lint; mod utils; @@ -411,108 +411,9 @@ mod zombie_processes; use clippy_config::{Conf, get_configuration_metadata, sanitize_explanation}; use clippy_utils::macros::FormatArgsStorage; use rustc_data_structures::fx::FxHashSet; -use rustc_lint::{Lint, LintId}; +use rustc_lint::Lint; use utils::attr_collector::{AttrCollector, AttrStorage}; -#[derive(Default)] -struct RegistrationGroups { - all: Vec<LintId>, - cargo: Vec<LintId>, - complexity: Vec<LintId>, - correctness: Vec<LintId>, - nursery: Vec<LintId>, - pedantic: Vec<LintId>, - perf: Vec<LintId>, - restriction: Vec<LintId>, - style: Vec<LintId>, - suspicious: Vec<LintId>, -} - -impl RegistrationGroups { - #[rustfmt::skip] - fn register(self, store: &mut rustc_lint::LintStore) { - store.register_group(true, "clippy::all", Some("clippy_all"), self.all); - store.register_group(true, "clippy::cargo", Some("clippy_cargo"), self.cargo); - store.register_group(true, "clippy::complexity", Some("clippy_complexity"), self.complexity); - store.register_group(true, "clippy::correctness", Some("clippy_correctness"), self.correctness); - store.register_group(true, "clippy::nursery", Some("clippy_nursery"), self.nursery); - store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), self.pedantic); - store.register_group(true, "clippy::perf", Some("clippy_perf"), self.perf); - store.register_group(true, "clippy::restriction", Some("clippy_restriction"), self.restriction); - store.register_group(true, "clippy::style", Some("clippy_style"), self.style); - store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), self.suspicious); - } -} - -#[derive(Copy, Clone, Debug)] -pub(crate) enum LintCategory { - Cargo, - Complexity, - Correctness, - Nursery, - Pedantic, - Perf, - Restriction, - Style, - Suspicious, -} - -#[allow(clippy::enum_glob_use)] -use LintCategory::*; - -impl LintCategory { - fn is_all(self) -> bool { - matches!(self, Correctness | Suspicious | Style | Complexity | Perf) - } - - fn group(self, groups: &mut RegistrationGroups) -> &mut Vec<LintId> { - match self { - Cargo => &mut groups.cargo, - Complexity => &mut groups.complexity, - Correctness => &mut groups.correctness, - Nursery => &mut groups.nursery, - Pedantic => &mut groups.pedantic, - Perf => &mut groups.perf, - Restriction => &mut groups.restriction, - Style => &mut groups.style, - Suspicious => &mut groups.suspicious, - } - } -} - -pub struct LintInfo { - /// Double reference to maintain pointer equality - pub lint: &'static &'static Lint, - category: LintCategory, - pub explanation: &'static str, - /// e.g. `clippy_lints/src/absolute_paths.rs#43` - pub location: &'static str, - pub version: Option<&'static str>, -} - -impl LintInfo { - /// Returns the lint name in lowercase without the `clippy::` prefix - #[allow(clippy::missing_panics_doc)] - pub fn name_lower(&self) -> String { - self.lint.name.strip_prefix("clippy::").unwrap().to_ascii_lowercase() - } - - /// Returns the name of the lint's category in lowercase (`style`, `pedantic`) - pub fn category_str(&self) -> &'static str { - match self.category { - Cargo => "cargo", - Complexity => "complexity", - Correctness => "correctness", - Nursery => "nursery", - Pedantic => "pedantic", - Perf => "perf", - Restriction => "restriction", - Style => "style", - Suspicious => "suspicious", - } - } -} - pub fn explain(name: &str) -> i32 { let target = format!("clippy::{}", name.to_ascii_uppercase()); @@ -535,30 +436,11 @@ pub fn explain(name: &str) -> i32 { } } -fn register_categories(store: &mut rustc_lint::LintStore) { - let mut groups = RegistrationGroups::default(); - - for LintInfo { lint, category, .. } in declared_lints::LINTS { - if category.is_all() { - groups.all.push(LintId::of(lint)); - } - - category.group(&mut groups).push(LintId::of(lint)); - } - - let lints: Vec<&'static Lint> = declared_lints::LINTS.iter().map(|info| *info.lint).collect(); - - store.register_lints(&lints); - groups.register(store); -} - /// Register all lints and lint groups with the rustc lint store /// /// Used in `./src/driver.rs`. #[expect(clippy::too_many_lines)] -pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { - register_categories(store); - +pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Conf) { for (old_name, new_name) in deprecated_lints::RENAMED { store.register_renamed(old_name, new_name); } diff --git a/src/tools/clippy/clippy_lints/src/loops/same_item_push.rs b/src/tools/clippy/clippy_lints/src/loops/same_item_push.rs index 388034c39f5..e792edbe23e 100644 --- a/src/tools/clippy/clippy_lints/src/loops/same_item_push.rs +++ b/src/tools/clippy/clippy_lints/src/loops/same_item_push.rs @@ -163,15 +163,14 @@ impl<'tcx> Visitor<'tcx> for SameItemPushVisitor<'_, 'tcx> { StmtKind::Expr(expr) | StmtKind::Semi(expr) => self.visit_expr(expr), _ => {}, } + } + // Current statement is a push ...check whether another + // push had been previously done + else if self.vec_push.is_none() { + self.vec_push = vec_push_option; } else { - // Current statement is a push ...check whether another - // push had been previously done - if self.vec_push.is_none() { - self.vec_push = vec_push_option; - } else { - // There are multiple pushes ... don't lint - self.multiple_pushes = true; - } + // There are multiple pushes ... don't lint + self.multiple_pushes = true; } } } diff --git a/src/tools/clippy/clippy_lints/src/manual_let_else.rs b/src/tools/clippy/clippy_lints/src/manual_let_else.rs index 0b3bec714c0..9ff82cdcb66 100644 --- a/src/tools/clippy/clippy_lints/src/manual_let_else.rs +++ b/src/tools/clippy/clippy_lints/src/manual_let_else.rs @@ -13,7 +13,6 @@ use rustc_errors::Applicability; use rustc_hir::def::{CtorOf, DefKind, Res}; use rustc_hir::{Arm, Expr, ExprKind, HirId, MatchSource, Pat, PatExpr, PatExprKind, PatKind, QPath, Stmt, StmtKind}; use rustc_lint::{LateContext, LintContext}; - use rustc_span::Span; use rustc_span::symbol::{Symbol, sym}; use std::slice; diff --git a/src/tools/clippy/clippy_lints/src/matches/manual_ok_err.rs b/src/tools/clippy/clippy_lints/src/matches/manual_ok_err.rs index 4959908dad6..edbb556fd97 100644 --- a/src/tools/clippy/clippy_lints/src/matches/manual_ok_err.rs +++ b/src/tools/clippy/clippy_lints/src/matches/manual_ok_err.rs @@ -1,9 +1,9 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::source::{indent_of, reindent_multiline}; use clippy_utils::sugg::Sugg; -use clippy_utils::ty::option_arg_ty; +use clippy_utils::ty::{option_arg_ty, peel_mid_ty_refs_is_mutable}; use clippy_utils::{get_parent_expr, is_res_lang_ctor, path_res, peel_blocks, span_contains_comment}; -use rustc_ast::BindingMode; +use rustc_ast::{BindingMode, Mutability}; use rustc_errors::Applicability; use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr}; use rustc_hir::def::{DefKind, Res}; @@ -133,7 +133,21 @@ fn apply_lint(cx: &LateContext<'_>, expr: &Expr<'_>, scrutinee: &Expr<'_>, is_ok Applicability::MachineApplicable }; let scrut = Sugg::hir_with_applicability(cx, scrutinee, "..", &mut app).maybe_paren(); - let sugg = format!("{scrut}.{method}()"); + + let scrutinee_ty = cx.typeck_results().expr_ty(scrutinee); + let (_, n_ref, mutability) = peel_mid_ty_refs_is_mutable(scrutinee_ty); + let prefix = if n_ref > 0 { + if mutability == Mutability::Mut { + ".as_mut()" + } else { + ".as_ref()" + } + } else { + "" + }; + + let sugg = format!("{scrut}{prefix}.{method}()"); + // If the expression being expanded is the `if …` part of an `else if …`, it must be blockified. let sugg = if let Some(parent_expr) = get_parent_expr(cx, expr) && let ExprKind::If(_, _, Some(else_part)) = parent_expr.kind diff --git a/src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs b/src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs index 24b4a675800..70a03ff9376 100644 --- a/src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs +++ b/src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs @@ -1,4 +1,5 @@ use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::SpanRangeExt; use clippy_utils::ty::is_type_diagnostic_item; use clippy_utils::{is_refutable, peel_hir_pat_refs, recurse_or_patterns}; use rustc_errors::Applicability; @@ -116,11 +117,12 @@ pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) { let format_suggestion = |variant: &VariantDef| { format!( "{}{}{}{}", - if let Some(ident) = wildcard_ident { - format!("{} @ ", ident.name) - } else { - String::new() - }, + wildcard_ident.map_or(String::new(), |ident| { + ident + .span + .get_source_text(cx) + .map_or_else(|| format!("{} @ ", ident.name), |s| format!("{s} @ ")) + }), if let CommonPrefixSearcher::Path(path_prefix) = path_prefix { let mut s = String::new(); for seg in path_prefix { @@ -138,7 +140,7 @@ pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) { Some(CtorKind::Fn) if variant.fields.len() == 1 => "(_)", Some(CtorKind::Fn) => "(..)", Some(CtorKind::Const) => "", - None => "{ .. }", + None => " { .. }", } ) }; diff --git a/src/tools/clippy/clippy_lints/src/methods/mod.rs b/src/tools/clippy/clippy_lints/src/methods/mod.rs index 347960e0003..f2dabdd3438 100644 --- a/src/tools/clippy/clippy_lints/src/methods/mod.rs +++ b/src/tools/clippy/clippy_lints/src/methods/mod.rs @@ -4426,7 +4426,7 @@ declare_clippy_lint! { /// ```no_run /// use std::io::{BufReader, Read}; /// use std::fs::File; - /// let file = BufReader::new(std::fs::File::open("./bytes.txt").unwrap()); + /// let file = BufReader::new(File::open("./bytes.txt").unwrap()); /// file.bytes(); /// ``` #[clippy::version = "1.87.0"] diff --git a/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs b/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs index 7bdd999bbba..2139466ce74 100644 --- a/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs +++ b/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs @@ -136,7 +136,7 @@ pub(super) fn check<'tcx>( fun_span: Option<Span>, ) -> bool { // (path, fn_has_argument, methods, suffix) - const KNOW_TYPES: [(Symbol, bool, &[Symbol], &str); 4] = [ + const KNOW_TYPES: [(Symbol, bool, &[Symbol], &str); 5] = [ (sym::BTreeEntry, false, &[sym::or_insert], "with"), (sym::HashMapEntry, false, &[sym::or_insert], "with"), ( @@ -145,16 +145,17 @@ pub(super) fn check<'tcx>( &[sym::map_or, sym::ok_or, sym::or, sym::unwrap_or], "else", ), - (sym::Result, true, &[sym::or, sym::unwrap_or], "else"), + (sym::Option, false, &[sym::get_or_insert], "with"), + (sym::Result, true, &[sym::map_or, sym::or, sym::unwrap_or], "else"), ]; if KNOW_TYPES.iter().any(|k| k.2.contains(&name)) && switch_to_lazy_eval(cx, arg) && !contains_return(arg) && let self_ty = cx.typeck_results().expr_ty(self_expr) - && let Some(&(_, fn_has_arguments, poss, suffix)) = - KNOW_TYPES.iter().find(|&&i| is_type_diagnostic_item(cx, self_ty, i.0)) - && poss.contains(&name) + && let Some(&(_, fn_has_arguments, _, suffix)) = KNOW_TYPES + .iter() + .find(|&&i| is_type_diagnostic_item(cx, self_ty, i.0) && i.2.contains(&name)) { let ctxt = span.ctxt(); let mut app = Applicability::HasPlaceholders; diff --git a/src/tools/clippy/clippy_lints/src/missing_inline.rs b/src/tools/clippy/clippy_lints/src/missing_inline.rs index f835bbb7c56..25c95d23436 100644 --- a/src/tools/clippy/clippy_lints/src/missing_inline.rs +++ b/src/tools/clippy/clippy_lints/src/missing_inline.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint; -use rustc_attr_data_structures::{find_attr, AttributeKind}; +use rustc_attr_data_structures::{AttributeKind, find_attr}; use rustc_hir as hir; use rustc_hir::Attribute; use rustc_lint::{LateContext, LateLintPass, LintContext}; diff --git a/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs b/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs index 8d2f8029112..17d251a7bbb 100644 --- a/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs +++ b/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs @@ -161,7 +161,7 @@ fn path_has_args(p: &QPath<'_>) -> bool { /// - `Copy` itself, or /// - the only use of a mutable reference, or /// - not a variable (created by a function call) -#[expect(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments, clippy::too_many_lines)] fn needless_borrow_count<'tcx>( cx: &LateContext<'tcx>, possible_borrowers: &mut Vec<(LocalDefId, PossibleBorrowerMap<'tcx, 'tcx>)>, @@ -232,11 +232,11 @@ fn needless_borrow_count<'tcx>( let mut args_with_referent_ty = callee_args.to_vec(); let mut check_reference_and_referent = |reference: &Expr<'tcx>, referent: &Expr<'tcx>| { - if let ExprKind::Field(base, _) = &referent.kind { - let base_ty = cx.typeck_results().expr_ty(base); - if drop_trait_def_id.is_some_and(|id| implements_trait(cx, base_ty, id, &[])) { - return false; - } + if let ExprKind::Field(base, _) = &referent.kind + && let base_ty = cx.typeck_results().expr_ty(base) + && drop_trait_def_id.is_some_and(|id| implements_trait(cx, base_ty, id, &[])) + { + return false; } let referent_ty = cx.typeck_results().expr_ty(referent); diff --git a/src/tools/clippy/clippy_lints/src/needless_parens_on_range_literals.rs b/src/tools/clippy/clippy_lints/src/needless_parens_on_range_literals.rs index 8a62106377c..021a11593f3 100644 --- a/src/tools/clippy/clippy_lints/src/needless_parens_on_range_literals.rs +++ b/src/tools/clippy/clippy_lints/src/needless_parens_on_range_literals.rs @@ -5,7 +5,6 @@ use clippy_utils::source::{snippet, snippet_with_applicability}; use rustc_ast::ast; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind}; - use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; diff --git a/src/tools/clippy/clippy_lints/src/needless_pass_by_value.rs b/src/tools/clippy/clippy_lints/src/needless_pass_by_value.rs index 9aede1dec93..c97ecce75b4 100644 --- a/src/tools/clippy/clippy_lints/src/needless_pass_by_value.rs +++ b/src/tools/clippy/clippy_lints/src/needless_pass_by_value.rs @@ -124,8 +124,10 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { // Note that we do not want to deal with qualified predicates here. match pred.kind().no_bound_vars() { Some(ty::ClauseKind::Trait(pred)) - if pred.def_id() != sized_trait && pred.def_id() != meta_sized_trait - => Some(pred), + if pred.def_id() != sized_trait && pred.def_id() != meta_sized_trait => + { + Some(pred) + }, _ => None, } }) diff --git a/src/tools/clippy/clippy_lints/src/no_mangle_with_rust_abi.rs b/src/tools/clippy/clippy_lints/src/no_mangle_with_rust_abi.rs index 0159c5d2ac1..dee8efeb291 100644 --- a/src/tools/clippy/clippy_lints/src/no_mangle_with_rust_abi.rs +++ b/src/tools/clippy/clippy_lints/src/no_mangle_with_rust_abi.rs @@ -1,13 +1,12 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::source::{snippet, snippet_with_applicability}; use rustc_abi::ExternAbi; +use rustc_attr_data_structures::AttributeKind; use rustc_errors::Applicability; -use rustc_hir::{Item, ItemKind}; +use rustc_hir::{Attribute, Item, ItemKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::{BytePos, Pos}; -use rustc_attr_data_structures::AttributeKind; -use rustc_hir::Attribute; declare_clippy_lint! { /// ### What it does diff --git a/src/tools/clippy/clippy_lints/src/non_copy_const.rs b/src/tools/clippy/clippy_lints/src/non_copy_const.rs index a27c6aa75e3..5f10e1968f1 100644 --- a/src/tools/clippy/clippy_lints/src/non_copy_const.rs +++ b/src/tools/clippy/clippy_lints/src/non_copy_const.rs @@ -617,7 +617,7 @@ impl<'tcx> NonCopyConst<'tcx> { // Then a type check. Note we only check the type here as the result // gets cached. - let ty = EarlyBinder::bind(typeck.expr_ty(src_expr)).instantiate(tcx, init_args); + let ty = typeck.expr_ty(src_expr); // Normalized as we need to check if this is an array later. let ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty); if self.is_ty_freeze(tcx, typing_env, ty).is_freeze() { diff --git a/src/tools/clippy/clippy_lints/src/operators/identity_op.rs b/src/tools/clippy/clippy_lints/src/operators/identity_op.rs index e1fd09549a4..3efbb896358 100644 --- a/src/tools/clippy/clippy_lints/src/operators/identity_op.rs +++ b/src/tools/clippy/clippy_lints/src/operators/identity_op.rs @@ -1,12 +1,13 @@ -use clippy_utils::consts::{ConstEvalCtxt, Constant, FullInt}; +use clippy_utils::consts::{ConstEvalCtxt, Constant, FullInt, integer_const, is_zero_integer_const}; use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::source::snippet_with_applicability; -use clippy_utils::{clip, peel_hir_expr_refs, unsext}; +use clippy_utils::{ExprUseNode, clip, expr_use_ctxt, peel_hir_expr_refs, unsext}; use rustc_errors::Applicability; -use rustc_hir::{BinOpKind, Expr, ExprKind, Node}; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{BinOpKind, Expr, ExprKind, Node, Path, QPath}; use rustc_lint::LateContext; use rustc_middle::ty; -use rustc_span::Span; +use rustc_span::{Span, kw}; use super::IDENTITY_OP; @@ -17,7 +18,7 @@ pub(crate) fn check<'tcx>( left: &'tcx Expr<'_>, right: &'tcx Expr<'_>, ) { - if !is_allowed(cx, op, left, right) { + if !is_allowed(cx, expr, op, left, right) { return; } @@ -165,14 +166,27 @@ fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, child: &Expr<'_>) Parens::Needed } -fn is_allowed(cx: &LateContext<'_>, cmp: BinOpKind, left: &Expr<'_>, right: &Expr<'_>) -> bool { +fn is_allowed<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, + cmp: BinOpKind, + left: &Expr<'tcx>, + right: &Expr<'tcx>, +) -> bool { + // Exclude case where the left or right side is associated function call returns a type which is + // `Self` that is not given explicitly, and the expression is not a let binding's init + // expression and the let binding has a type annotation, or a function's return value. + if (is_assoc_fn_without_type_instance(cx, left) || is_assoc_fn_without_type_instance(cx, right)) + && !is_expr_used_with_type_annotation(cx, expr) + { + return false; + } + // This lint applies to integers and their references cx.typeck_results().expr_ty(left).peel_refs().is_integral() && cx.typeck_results().expr_ty(right).peel_refs().is_integral() // `1 << 0` is a common pattern in bit manipulation code - && !(cmp == BinOpKind::Shl - && ConstEvalCtxt::new(cx).eval_simple(right) == Some(Constant::Int(0)) - && ConstEvalCtxt::new(cx).eval_simple(left) == Some(Constant::Int(1))) + && !(cmp == BinOpKind::Shl && is_zero_integer_const(cx, right) && integer_const(cx, left) == Some(1)) } fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span: Span, arg: Span) { @@ -234,3 +248,47 @@ fn span_ineffective_operation( applicability, ); } + +fn is_expr_used_with_type_annotation<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { + match expr_use_ctxt(cx, expr).use_node(cx) { + ExprUseNode::LetStmt(letstmt) => letstmt.ty.is_some(), + ExprUseNode::Return(_) => true, + _ => false, + } +} + +/// Check if the expression is an associated function without a type instance. +/// Example: +/// ``` +/// trait Def { +/// fn def() -> Self; +/// } +/// impl Def for usize { +/// fn def() -> Self { +/// 0 +/// } +/// } +/// fn test() { +/// let _ = 0usize + &Default::default(); +/// let _ = 0usize + &Def::def(); +/// } +/// ``` +fn is_assoc_fn_without_type_instance<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool { + if let ExprKind::Call(func, _) = peel_hir_expr_refs(expr).0.kind + && let ExprKind::Path(QPath::Resolved( + // If it's not None, don't need to go further. + None, + Path { + res: Res::Def(DefKind::AssocFn, def_id), + .. + }, + )) = func.kind + && let output_ty = cx.tcx.fn_sig(def_id).instantiate_identity().skip_binder().output() + && let ty::Param(ty::ParamTy { + name: kw::SelfUpper, .. + }) = output_ty.kind() + { + return true; + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/operators/manual_is_multiple_of.rs b/src/tools/clippy/clippy_lints/src/operators/manual_is_multiple_of.rs new file mode 100644 index 00000000000..821178a4315 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/manual_is_multiple_of.rs @@ -0,0 +1,66 @@ +use clippy_utils::consts::is_zero_integer_const; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::msrvs::{self, Msrv}; +use clippy_utils::sugg::Sugg; +use rustc_ast::BinOpKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::MANUAL_IS_MULTIPLE_OF; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'_>, + op: BinOpKind, + lhs: &'tcx Expr<'tcx>, + rhs: &'tcx Expr<'tcx>, + msrv: Msrv, +) { + if msrv.meets(cx, msrvs::UNSIGNED_IS_MULTIPLE_OF) + && let Some(operand) = uint_compare_to_zero(cx, op, lhs, rhs) + && let ExprKind::Binary(operand_op, operand_left, operand_right) = operand.kind + && operand_op.node == BinOpKind::Rem + { + let mut app = Applicability::MachineApplicable; + let divisor = Sugg::hir_with_applicability(cx, operand_right, "_", &mut app); + span_lint_and_sugg( + cx, + MANUAL_IS_MULTIPLE_OF, + expr.span, + "manual implementation of `.is_multiple_of()`", + "replace with", + format!( + "{}{}.is_multiple_of({divisor})", + if op == BinOpKind::Eq { "" } else { "!" }, + Sugg::hir_with_applicability(cx, operand_left, "_", &mut app).maybe_paren() + ), + app, + ); + } +} + +// If we have a `x == 0`, `x != 0` or `x > 0` (or the reverted ones), return the non-zero operand +fn uint_compare_to_zero<'tcx>( + cx: &LateContext<'tcx>, + op: BinOpKind, + lhs: &'tcx Expr<'tcx>, + rhs: &'tcx Expr<'tcx>, +) -> Option<&'tcx Expr<'tcx>> { + let operand = if matches!(lhs.kind, ExprKind::Binary(..)) + && matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Gt) + && is_zero_integer_const(cx, rhs) + { + lhs + } else if matches!(rhs.kind, ExprKind::Binary(..)) + && matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Lt) + && is_zero_integer_const(cx, lhs) + { + rhs + } else { + return None; + }; + + matches!(cx.typeck_results().expr_ty_adjusted(operand).kind(), ty::Uint(_)).then_some(operand) +} diff --git a/src/tools/clippy/clippy_lints/src/operators/mod.rs b/src/tools/clippy/clippy_lints/src/operators/mod.rs index 2f4e8e99588..bdbbb3475cd 100644 --- a/src/tools/clippy/clippy_lints/src/operators/mod.rs +++ b/src/tools/clippy/clippy_lints/src/operators/mod.rs @@ -11,6 +11,7 @@ mod float_cmp; mod float_equality_without_abs; mod identity_op; mod integer_division; +mod manual_is_multiple_of; mod manual_midpoint; mod misrefactored_assign_op; mod modulo_arithmetic; @@ -830,12 +831,42 @@ declare_clippy_lint! { "manual implementation of `midpoint` which can overflow" } +declare_clippy_lint! { + /// ### What it does + /// Checks for manual implementation of `.is_multiple_of()` on + /// unsigned integer types. + /// + /// ### Why is this bad? + /// `a.is_multiple_of(b)` is a clearer way to check for divisibility + /// of `a` by `b`. This expression can never panic. + /// + /// ### Example + /// ```no_run + /// # let (a, b) = (3u64, 4u64); + /// if a % b == 0 { + /// println!("{a} is divisible by {b}"); + /// } + /// ``` + /// Use instead: + /// ```no_run + /// # let (a, b) = (3u64, 4u64); + /// if a.is_multiple_of(b) { + /// println!("{a} is divisible by {b}"); + /// } + /// ``` + #[clippy::version = "1.89.0"] + pub MANUAL_IS_MULTIPLE_OF, + complexity, + "manual implementation of `.is_multiple_of()`" +} + pub struct Operators { arithmetic_context: numeric_arithmetic::Context, verbose_bit_mask_threshold: u64, modulo_arithmetic_allow_comparison_to_zero: bool, msrv: Msrv, } + impl Operators { pub fn new(conf: &'static Conf) -> Self { Self { @@ -874,6 +905,7 @@ impl_lint_pass!(Operators => [ NEEDLESS_BITWISE_BOOL, SELF_ASSIGNMENT, MANUAL_MIDPOINT, + MANUAL_IS_MULTIPLE_OF, ]); impl<'tcx> LateLintPass<'tcx> for Operators { @@ -891,6 +923,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators { identity_op::check(cx, e, op.node, lhs, rhs); 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); } self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs); bit_mask::check(cx, e, op.node, lhs, rhs); diff --git a/src/tools/clippy/clippy_lints/src/pass_by_ref_or_value.rs b/src/tools/clippy/clippy_lints/src/pass_by_ref_or_value.rs index e18bdfb34ac..b8005dfd6f8 100644 --- a/src/tools/clippy/clippy_lints/src/pass_by_ref_or_value.rs +++ b/src/tools/clippy/clippy_lints/src/pass_by_ref_or_value.rs @@ -3,10 +3,10 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::source::snippet; use clippy_utils::ty::{for_each_top_level_late_bound_region, is_copy}; use clippy_utils::{is_self, is_self_ty}; -use rustc_attr_data_structures::{find_attr, AttributeKind, InlineAttr}; -use rustc_data_structures::fx::FxHashSet; use core::ops::ControlFlow; use rustc_abi::ExternAbi; +use rustc_attr_data_structures::{AttributeKind, InlineAttr, find_attr}; +use rustc_data_structures::fx::FxHashSet; use rustc_errors::Applicability; use rustc_hir as hir; use rustc_hir::intravisit::FnKind; diff --git a/src/tools/clippy/clippy_lints/src/question_mark.rs b/src/tools/clippy/clippy_lints/src/question_mark.rs index c02e5e0621c..de12a25b03d 100644 --- a/src/tools/clippy/clippy_lints/src/question_mark.rs +++ b/src/tools/clippy/clippy_lints/src/question_mark.rs @@ -142,6 +142,7 @@ fn check_let_some_else_return_none(cx: &LateContext<'_>, stmt: &Stmt<'_>) { && let Some(ret) = find_let_else_ret_expression(els) && let Some(inner_pat) = pat_and_expr_can_be_question_mark(cx, pat, ret) && !span_contains_comment(cx.tcx.sess.source_map(), els.span) + && !span_contains_cfg(cx, els.span) { let mut applicability = Applicability::MaybeIncorrect; let init_expr_str = Sugg::hir_with_applicability(cx, init_expr, "..", &mut applicability).maybe_paren(); diff --git a/src/tools/clippy/clippy_lints/src/question_mark_used.rs b/src/tools/clippy/clippy_lints/src/question_mark_used.rs index 96ea485d769..7bbbd0d25ac 100644 --- a/src/tools/clippy/clippy_lints/src/question_mark_used.rs +++ b/src/tools/clippy/clippy_lints/src/question_mark_used.rs @@ -1,5 +1,4 @@ use clippy_utils::diagnostics::span_lint_and_then; - use clippy_utils::macros::span_is_local; use rustc_hir::{Expr, ExprKind, MatchSource}; use rustc_lint::{LateContext, LateLintPass}; diff --git a/src/tools/clippy/clippy_lints/src/read_zero_byte_vec.rs b/src/tools/clippy/clippy_lints/src/read_zero_byte_vec.rs index 6b1dc864fb7..acd840401c6 100644 --- a/src/tools/clippy/clippy_lints/src/read_zero_byte_vec.rs +++ b/src/tools/clippy/clippy_lints/src/read_zero_byte_vec.rs @@ -3,11 +3,10 @@ use clippy_utils::higher::{VecInitKind, get_vec_init_kind}; use clippy_utils::source::snippet; use clippy_utils::{get_enclosing_block, sym}; -use hir::{Expr, ExprKind, HirId, LetStmt, PatKind, PathSegment, QPath, StmtKind}; use rustc_errors::Applicability; -use rustc_hir as hir; use rustc_hir::def::Res; use rustc_hir::intravisit::{Visitor, walk_expr}; +use rustc_hir::{self as hir, Expr, ExprKind, HirId, LetStmt, PatKind, PathSegment, QPath, StmtKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; diff --git a/src/tools/clippy/clippy_lints/src/return_self_not_must_use.rs b/src/tools/clippy/clippy_lints/src/return_self_not_must_use.rs index 1b304dc5768..25929b853af 100644 --- a/src/tools/clippy/clippy_lints/src/return_self_not_must_use.rs +++ b/src/tools/clippy/clippy_lints/src/return_self_not_must_use.rs @@ -1,14 +1,13 @@ use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::ty::is_must_use_ty; use clippy_utils::{nth_arg, return_ty}; +use rustc_attr_data_structures::{AttributeKind, find_attr}; use rustc_hir::def_id::LocalDefId; use rustc_hir::intravisit::FnKind; use rustc_hir::{Body, FnDecl, OwnerId, TraitItem, TraitItemKind}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_session::declare_lint_pass; -use rustc_span::{Span}; -use rustc_attr_data_structures::AttributeKind; -use rustc_attr_data_structures::find_attr; +use rustc_span::Span; declare_clippy_lint! { /// ### What it does diff --git a/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs b/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs index 62939912304..38cf7e3822a 100644 --- a/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs +++ b/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs @@ -219,22 +219,21 @@ impl SingleComponentPathImports { } } } - } else { - // keep track of `use self::some_module` usages - if segments[0].ident.name == kw::SelfLower { - // simple case such as `use self::module::SomeStruct` - if segments.len() > 1 { - imports_reused_with_self.push(segments[1].ident.name); - return; - } + } + // keep track of `use self::some_module` usages + else if segments[0].ident.name == kw::SelfLower { + // simple case such as `use self::module::SomeStruct` + if segments.len() > 1 { + imports_reused_with_self.push(segments[1].ident.name); + return; + } - // nested case such as `use self::{module1::Struct1, module2::Struct2}` - if let UseTreeKind::Nested { items, .. } = &use_tree.kind { - for tree in items { - let segments = &tree.0.prefix.segments; - if !segments.is_empty() { - imports_reused_with_self.push(segments[0].ident.name); - } + // nested case such as `use self::{module1::Struct1, module2::Struct2}` + if let UseTreeKind::Nested { items, .. } = &use_tree.kind { + for tree in items { + let segments = &tree.0.prefix.segments; + if !segments.is_empty() { + imports_reused_with_self.push(segments[0].ident.name); } } } diff --git a/src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs b/src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs index a2938c86c76..92427473a8e 100644 --- a/src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs +++ b/src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs @@ -606,32 +606,31 @@ fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span let ctxt = span.ctxt(); if ctxt == SyntaxContext::root() { HasSafetyComment::Maybe - } else { - // From a macro expansion. Get the text from the start of the macro declaration to start of the - // unsafe block. - // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; } - // ^--------------------------------------------^ - if let Ok(unsafe_line) = source_map.lookup_line(span.lo()) - && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo()) - && Arc::ptr_eq(&unsafe_line.sf, ¯o_line.sf) - && let Some(src) = unsafe_line.sf.src.as_deref() - { - if macro_line.line < unsafe_line.line { - match text_has_safety_comment( - src, - &unsafe_line.sf.lines()[macro_line.line + 1..=unsafe_line.line], - unsafe_line.sf.start_pos, - ) { - Some(b) => HasSafetyComment::Yes(b), - None => HasSafetyComment::No, - } - } else { - HasSafetyComment::No + } + // From a macro expansion. Get the text from the start of the macro declaration to start of the + // unsafe block. + // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; } + // ^--------------------------------------------^ + else if let Ok(unsafe_line) = source_map.lookup_line(span.lo()) + && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo()) + && Arc::ptr_eq(&unsafe_line.sf, ¯o_line.sf) + && let Some(src) = unsafe_line.sf.src.as_deref() + { + if macro_line.line < unsafe_line.line { + match text_has_safety_comment( + src, + &unsafe_line.sf.lines()[macro_line.line + 1..=unsafe_line.line], + unsafe_line.sf.start_pos, + ) { + Some(b) => HasSafetyComment::Yes(b), + None => HasSafetyComment::No, } } else { - // Problem getting source text. Pretend a comment was found. - HasSafetyComment::Maybe + HasSafetyComment::No } + } else { + // Problem getting source text. Pretend a comment was found. + HasSafetyComment::Maybe } } diff --git a/src/tools/clippy/clippy_utils/Cargo.toml b/src/tools/clippy/clippy_utils/Cargo.toml index 615c0995e8b..73291aa8cdf 100644 --- a/src/tools/clippy/clippy_utils/Cargo.toml +++ b/src/tools/clippy/clippy_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clippy_utils" -version = "0.1.89" +version = "0.1.90" edition = "2024" description = "Helpful tools for writing lints, provided as they are used in Clippy" repository = "https://github.com/rust-lang/rust-clippy" diff --git a/src/tools/clippy/clippy_utils/README.md b/src/tools/clippy/clippy_utils/README.md index 1aa16e3943c..649748d1534 100644 --- a/src/tools/clippy/clippy_utils/README.md +++ b/src/tools/clippy/clippy_utils/README.md @@ -8,7 +8,7 @@ This crate is only guaranteed to build with this `nightly` toolchain: <!-- begin autogenerated nightly --> ``` -nightly-2025-06-12 +nightly-2025-06-26 ``` <!-- end autogenerated nightly --> diff --git a/src/tools/clippy/clippy_utils/src/consts.rs b/src/tools/clippy/clippy_utils/src/consts.rs index 1ec5d11384f..aaa071fd5c9 100644 --- a/src/tools/clippy/clippy_utils/src/consts.rs +++ b/src/tools/clippy/clippy_utils/src/consts.rs @@ -958,3 +958,18 @@ fn field_of_struct<'tcx>( None } } + +/// If `expr` evaluates to an integer constant, return its value. +pub fn integer_const(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> { + if let Some(Constant::Int(value)) = ConstEvalCtxt::new(cx).eval_simple(expr) { + Some(value) + } else { + None + } +} + +/// Check if `expr` evaluates to an integer constant of 0. +#[inline] +pub fn is_zero_integer_const(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + integer_const(cx, expr) == Some(0) +} diff --git a/src/tools/clippy/clippy_utils/src/diagnostics.rs b/src/tools/clippy/clippy_utils/src/diagnostics.rs index cd2098a8989..dc240dd067b 100644 --- a/src/tools/clippy/clippy_utils/src/diagnostics.rs +++ b/src/tools/clippy/clippy_utils/src/diagnostics.rs @@ -109,7 +109,7 @@ pub fn span_lint<T: LintContext>(cx: &T, lint: &'static Lint, sp: impl Into<Mult }); } -/// Same as `span_lint` but with an extra `help` message. +/// Same as [`span_lint`] but with an extra `help` message. /// /// Use this if you want to provide some general help but /// can't provide a specific machine applicable suggestion. @@ -166,7 +166,7 @@ pub fn span_lint_and_help<T: LintContext>( }); } -/// Like `span_lint` but with a `note` section instead of a `help` message. +/// Like [`span_lint`] but with a `note` section instead of a `help` message. /// /// The `note` message is presented separately from the main lint message /// and is attached to a specific span: @@ -226,7 +226,7 @@ pub fn span_lint_and_note<T: LintContext>( }); } -/// Like `span_lint` but allows to add notes, help and suggestions using a closure. +/// Like [`span_lint`] but allows to add notes, help and suggestions using a closure. /// /// If you need to customize your lint output a lot, use this function. /// If you change the signature, remember to update the internal lint `CollapsibleCalls` diff --git a/src/tools/clippy/clippy_utils/src/lib.rs b/src/tools/clippy/clippy_utils/src/lib.rs index 913589319fc..a8b33418c8c 100644 --- a/src/tools/clippy/clippy_utils/src/lib.rs +++ b/src/tools/clippy/clippy_utils/src/lib.rs @@ -122,7 +122,7 @@ use rustc_span::hygiene::{ExpnKind, MacroKind}; use rustc_span::source_map::SourceMap; use rustc_span::symbol::{Ident, Symbol, kw}; use rustc_span::{InnerSpan, Span}; -use source::walk_span_to_context; +use source::{SpanRangeExt, walk_span_to_context}; use visitors::{Visitable, for_each_unconsumed_temporary}; use crate::consts::{ConstEvalCtxt, Constant, mir_to_const}; @@ -1886,10 +1886,7 @@ pub fn is_must_use_func_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { _ => None, }; - did.is_some_and(|did| find_attr!( - cx.tcx.get_all_attrs(did), - AttributeKind::MustUse { ..} - )) + did.is_some_and(|did| find_attr!(cx.tcx.get_all_attrs(did), AttributeKind::MustUse { .. })) } /// Checks if a function's body represents the identity function. Looks for bodies of the form: @@ -2713,7 +2710,7 @@ impl<'tcx> ExprUseNode<'tcx> { } /// Gets the context an expression's value is used in. -pub fn expr_use_ctxt<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) -> ExprUseCtxt<'tcx> { +pub fn expr_use_ctxt<'tcx>(cx: &LateContext<'tcx>, e: &Expr<'tcx>) -> ExprUseCtxt<'tcx> { let mut adjustments = [].as_slice(); let mut is_ty_unified = false; let mut moved_before_use = false; @@ -2790,6 +2787,19 @@ pub fn span_contains_comment(sm: &SourceMap, span: Span) -> bool { }); } +/// Checks whether a given span has any significant token. A significant token is a non-whitespace +/// token, including comments unless `skip_comments` is set. +/// This is useful to determine if there are any actual code tokens in the span that are omitted in +/// the late pass, such as platform-specific code. +pub fn span_contains_non_whitespace(cx: &impl source::HasSession, span: Span, skip_comments: bool) -> bool { + matches!(span.get_source_text(cx), Some(snippet) if tokenize_with_text(&snippet).any(|(token, _, _)| + match token { + TokenKind::Whitespace => false, + TokenKind::BlockComment { .. } | TokenKind::LineComment { .. } => !skip_comments, + _ => true, + } + )) +} /// Returns all the comments a given span contains /// /// Comments are returned wrapped with their relevant delimiters diff --git a/src/tools/clippy/clippy_utils/src/msrvs.rs b/src/tools/clippy/clippy_utils/src/msrvs.rs index a5e66ad463b..7a0bef1a9bb 100644 --- a/src/tools/clippy/clippy_utils/src/msrvs.rs +++ b/src/tools/clippy/clippy_utils/src/msrvs.rs @@ -24,7 +24,7 @@ macro_rules! msrv_aliases { // names may refer to stabilized feature flags or library items msrv_aliases! { 1,88,0 { LET_CHAINS } - 1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT } + 1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT, UNSIGNED_IS_MULTIPLE_OF } 1,85,0 { UINT_FLOAT_MIDPOINT, CONST_SIZE_OF_VAL } 1,84,0 { CONST_OPTION_AS_SLICE, MANUAL_DANGLING_PTR } 1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP } @@ -42,6 +42,7 @@ msrv_aliases! { 1,65,0 { LET_ELSE, POINTER_CAST_CONSTNESS } 1,63,0 { CLONE_INTO, CONST_SLICE_FROM_REF } 1,62,0 { BOOL_THEN_SOME, DEFAULT_ENUM_ATTRIBUTE, CONST_EXTERN_C_FN } + 1,61,0 { CONST_FN_TRAIT_BOUND } 1,60,0 { ABS_DIFF } 1,59,0 { THREAD_LOCAL_CONST_INIT } 1,58,0 { FORMAT_ARGS_CAPTURE, PATTERN_TRAIT_CHAR_ARRAY, CONST_RAW_PTR_DEREF } diff --git a/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs b/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs index e629012b187..8f1ebb8ada6 100644 --- a/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs +++ b/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs @@ -32,6 +32,21 @@ pub fn is_min_const_fn<'tcx>(cx: &LateContext<'tcx>, body: &Body<'tcx>, msrv: Ms for local in &body.local_decls { check_ty(cx, local.ty, local.source_info.span, msrv)?; } + if !msrv.meets(cx, msrvs::CONST_FN_TRAIT_BOUND) + && let Some(sized_did) = cx.tcx.lang_items().sized_trait() + && let Some(meta_sized_did) = cx.tcx.lang_items().meta_sized_trait() + && cx.tcx.param_env(def_id).caller_bounds().iter().any(|bound| { + bound.as_trait_clause().is_some_and(|clause| { + let did = clause.def_id(); + did != sized_did && did != meta_sized_did + }) + }) + { + return Err(( + body.span, + "non-`Sized` trait clause before `const_fn_trait_bound` is stabilized".into(), + )); + } // impl trait is gone in MIR, so check the return type manually check_ty( cx, @@ -436,7 +451,7 @@ fn is_ty_const_destruct<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, body: &Body<'tcx> // FIXME(const_trait_impl, fee1-dead) revert to const destruct once it works again #[expect(unused)] fn is_ty_const_destruct_unused<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, body: &Body<'tcx>) -> bool { - // If this doesn't need drop at all, then don't select `~const Destruct`. + // If this doesn't need drop at all, then don't select `[const] Destruct`. if !ty.needs_drop(tcx, body.typing_env(tcx)) { return false; } diff --git a/src/tools/clippy/clippy_utils/src/sugg.rs b/src/tools/clippy/clippy_utils/src/sugg.rs index 6974e6512e2..7a24d07fa1d 100644 --- a/src/tools/clippy/clippy_utils/src/sugg.rs +++ b/src/tools/clippy/clippy_utils/src/sugg.rs @@ -494,7 +494,17 @@ impl<T: Display> Display for ParenHelper<T> { /// operators have the same /// precedence. pub fn make_unop(op: &str, expr: Sugg<'_>) -> Sugg<'static> { - Sugg::MaybeParen(format!("{op}{}", expr.maybe_paren()).into()) + // If the `expr` starts with `op` already, do not add wrap it in + // parentheses. + let expr = if let Sugg::MaybeParen(ref sugg) = expr + && !has_enclosing_paren(sugg) + && sugg.starts_with(op) + { + expr + } else { + expr.maybe_paren() + }; + Sugg::MaybeParen(format!("{op}{expr}").into()) } /// Builds the string for `<lhs> <op> <rhs>` adding parenthesis when necessary. @@ -1016,6 +1026,16 @@ mod test { let sugg = Sugg::BinOp(AssocOp::Binary(ast::BinOpKind::Add), "(1 + 1)".into(), "(1 + 1)".into()); assert_eq!("((1 + 1) + (1 + 1))", sugg.maybe_paren().to_string()); } + + #[test] + fn unop_parenthesize() { + let sugg = Sugg::NonParen("x".into()).mut_addr(); + assert_eq!("&mut x", sugg.to_string()); + let sugg = sugg.mut_addr(); + assert_eq!("&mut &mut x", sugg.to_string()); + assert_eq!("(&mut &mut x)", sugg.maybe_paren().to_string()); + } + #[test] fn not_op() { use ast::BinOpKind::{Add, And, Eq, Ge, Gt, Le, Lt, Ne, Or}; diff --git a/src/tools/clippy/clippy_utils/src/sym.rs b/src/tools/clippy/clippy_utils/src/sym.rs index 3b58dba5628..8a8218c6976 100644 --- a/src/tools/clippy/clippy_utils/src/sym.rs +++ b/src/tools/clippy/clippy_utils/src/sym.rs @@ -46,7 +46,6 @@ generate! { DOUBLE_QUOTE: "\"", Deserialize, EarlyLintPass, - ErrorKind, IntoIter, Itertools, LF: "\n", @@ -65,7 +64,6 @@ generate! { RegexBuilder, RegexSet, Start, - Step, Symbol, SyntaxContext, TBD, @@ -158,7 +156,6 @@ generate! { from_ne_bytes, from_ptr, from_raw, - from_ref, from_str, from_str_radix, fs, @@ -166,6 +163,7 @@ generate! { futures_util, get, get_mut, + get_or_insert, get_or_insert_with, get_unchecked, get_unchecked_mut, @@ -216,7 +214,6 @@ generate! { max_by_key, max_value, maximum, - mem, min, min_by, min_by_key, diff --git a/src/tools/clippy/clippy_utils/src/ty/mod.rs b/src/tools/clippy/clippy_utils/src/ty/mod.rs index 782b079ce09..bffbcf073ab 100644 --- a/src/tools/clippy/clippy_utils/src/ty/mod.rs +++ b/src/tools/clippy/clippy_utils/src/ty/mod.rs @@ -6,6 +6,7 @@ use core::ops::ControlFlow; use itertools::Itertools; use rustc_abi::VariantIdx; use rustc_ast::ast::Mutability; +use rustc_attr_data_structures::{AttributeKind, find_attr}; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_hir as hir; use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res}; @@ -20,8 +21,8 @@ use rustc_middle::traits::EvaluationResult; use rustc_middle::ty::layout::ValidityRequirement; use rustc_middle::ty::{ self, AdtDef, AliasTy, AssocItem, AssocTag, Binder, BoundRegion, FnSig, GenericArg, GenericArgKind, GenericArgsRef, - GenericParamDefKind, IntTy, Region, RegionKind, TraitRef, Ty, TyCtxt, TypeSuperVisitable, - TypeVisitable, TypeVisitableExt, TypeVisitor, UintTy, Upcast, VariantDef, VariantDiscr, + GenericParamDefKind, IntTy, Region, RegionKind, TraitRef, Ty, TyCtxt, TypeSuperVisitable, TypeVisitable, + TypeVisitableExt, TypeVisitor, UintTy, Upcast, VariantDef, VariantDiscr, }; use rustc_span::symbol::Ident; use rustc_span::{DUMMY_SP, Span, Symbol, sym}; @@ -31,8 +32,6 @@ use rustc_trait_selection::traits::{Obligation, ObligationCause}; use std::assert_matches::debug_assert_matches; use std::collections::hash_map::Entry; use std::iter; -use rustc_attr_data_structures::find_attr; -use rustc_attr_data_structures::AttributeKind; use crate::path_res; use crate::paths::{PathNS, lookup_path_str}; @@ -328,14 +327,8 @@ pub fn has_drop<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { // Returns whether the type has #[must_use] attribute pub fn is_must_use_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { match ty.kind() { - ty::Adt(adt, _) => find_attr!( - cx.tcx.get_all_attrs(adt.did()), - AttributeKind::MustUse { ..} - ), - ty::Foreign(did) => find_attr!( - cx.tcx.get_all_attrs(*did), - AttributeKind::MustUse { ..} - ), + ty::Adt(adt, _) => find_attr!(cx.tcx.get_all_attrs(adt.did()), AttributeKind::MustUse { .. }), + ty::Foreign(did) => find_attr!(cx.tcx.get_all_attrs(*did), AttributeKind::MustUse { .. }), ty::Slice(ty) | ty::Array(ty, _) | ty::RawPtr(ty, _) | ty::Ref(_, ty, _) => { // for the Array case we don't need to care for the len == 0 case // because we don't want to lint functions returning empty arrays @@ -345,7 +338,10 @@ pub fn is_must_use_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { ty::Alias(ty::Opaque, AliasTy { def_id, .. }) => { for (predicate, _) in cx.tcx.explicit_item_self_bounds(def_id).skip_binder() { if let ty::ClauseKind::Trait(trait_predicate) = predicate.kind().skip_binder() - && find_attr!(cx.tcx.get_all_attrs(trait_predicate.trait_ref.def_id), AttributeKind::MustUse { ..}) + && find_attr!( + cx.tcx.get_all_attrs(trait_predicate.trait_ref.def_id), + AttributeKind::MustUse { .. } + ) { return true; } @@ -355,7 +351,7 @@ pub fn is_must_use_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { ty::Dynamic(binder, _, _) => { for predicate in *binder { if let ty::ExistentialPredicate::Trait(ref trait_ref) = predicate.skip_binder() - && find_attr!(cx.tcx.get_all_attrs(trait_ref.def_id), AttributeKind::MustUse { ..}) + && find_attr!(cx.tcx.get_all_attrs(trait_ref.def_id), AttributeKind::MustUse { .. }) { return true; } diff --git a/src/tools/clippy/declare_clippy_lint/Cargo.toml b/src/tools/clippy/declare_clippy_lint/Cargo.toml new file mode 100644 index 00000000000..bd6b4dfdee4 --- /dev/null +++ b/src/tools/clippy/declare_clippy_lint/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "declare_clippy_lint" +version = "0.1.90" +edition = "2024" +repository = "https://github.com/rust-lang/rust-clippy" +license = "MIT OR Apache-2.0" + +[package.metadata.rust-analyzer] +# This crate uses #[feature(rustc_private)] +rustc_private = true diff --git a/src/tools/clippy/declare_clippy_lint/src/lib.rs b/src/tools/clippy/declare_clippy_lint/src/lib.rs new file mode 100644 index 00000000000..f7d9c64bfbd --- /dev/null +++ b/src/tools/clippy/declare_clippy_lint/src/lib.rs @@ -0,0 +1,280 @@ +#![feature(macro_metavar_expr_concat, rustc_private)] + +extern crate rustc_lint; + +use rustc_lint::{Lint, LintId, LintStore}; + +// Needed by `declare_clippy_lint!`. +pub extern crate rustc_session; + +#[derive(Default)] +pub struct LintListBuilder { + lints: Vec<&'static Lint>, + all: Vec<LintId>, + cargo: Vec<LintId>, + complexity: Vec<LintId>, + correctness: Vec<LintId>, + nursery: Vec<LintId>, + pedantic: Vec<LintId>, + perf: Vec<LintId>, + restriction: Vec<LintId>, + style: Vec<LintId>, + suspicious: Vec<LintId>, +} +impl LintListBuilder { + pub fn insert(&mut self, lints: &[&LintInfo]) { + #[allow(clippy::enum_glob_use)] + use LintCategory::*; + + self.lints.extend(lints.iter().map(|&x| x.lint)); + for &&LintInfo { lint, category, .. } in lints { + let (all, cat) = match category { + Complexity => (Some(&mut self.all), &mut self.complexity), + Correctness => (Some(&mut self.all), &mut self.correctness), + Perf => (Some(&mut self.all), &mut self.perf), + Style => (Some(&mut self.all), &mut self.style), + Suspicious => (Some(&mut self.all), &mut self.suspicious), + Cargo => (None, &mut self.cargo), + Nursery => (None, &mut self.nursery), + Pedantic => (None, &mut self.pedantic), + Restriction => (None, &mut self.restriction), + }; + if let Some(all) = all { + all.push(LintId::of(lint)); + } + cat.push(LintId::of(lint)); + } + } + + pub fn register(self, store: &mut LintStore) { + store.register_lints(&self.lints); + store.register_group(true, "clippy::all", Some("clippy_all"), self.all); + store.register_group(true, "clippy::cargo", Some("clippy_cargo"), self.cargo); + store.register_group(true, "clippy::complexity", Some("clippy_complexity"), self.complexity); + store.register_group( + true, + "clippy::correctness", + Some("clippy_correctness"), + self.correctness, + ); + store.register_group(true, "clippy::nursery", Some("clippy_nursery"), self.nursery); + store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), self.pedantic); + store.register_group(true, "clippy::perf", Some("clippy_perf"), self.perf); + store.register_group( + true, + "clippy::restriction", + Some("clippy_restriction"), + self.restriction, + ); + store.register_group(true, "clippy::style", Some("clippy_style"), self.style); + store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), self.suspicious); + } +} + +#[derive(Copy, Clone, Debug)] +pub enum LintCategory { + Cargo, + Complexity, + Correctness, + Nursery, + Pedantic, + Perf, + Restriction, + Style, + Suspicious, +} +impl LintCategory { + #[must_use] + pub fn name(self) -> &'static str { + match self { + Self::Cargo => "cargo", + Self::Complexity => "complexity", + Self::Correctness => "correctness", + Self::Nursery => "nursery", + Self::Pedantic => "pedantic", + Self::Perf => "perf", + Self::Restriction => "restriction", + Self::Style => "style", + Self::Suspicious => "suspicious", + } + } +} + +pub struct LintInfo { + pub lint: &'static Lint, + pub category: LintCategory, + pub explanation: &'static str, + /// e.g. `clippy_lints/src/absolute_paths.rs#43` + pub location: &'static str, + pub version: &'static str, +} + +impl LintInfo { + /// Returns the lint name in lowercase without the `clippy::` prefix + #[must_use] + #[expect(clippy::missing_panics_doc)] + pub fn name_lower(&self) -> String { + self.lint.name.strip_prefix("clippy::").unwrap().to_ascii_lowercase() + } +} + +#[macro_export] +macro_rules! declare_clippy_lint_inner { + ( + $(#[doc = $docs:literal])* + #[clippy::version = $version:literal] + $vis:vis $lint_name:ident, + $level:ident, + $category:ident, + $desc:literal + $(, @eval_always = $eval_always:literal)? + ) => { + $crate::rustc_session::declare_tool_lint! { + $(#[doc = $docs])* + #[clippy::version = $version] + $vis clippy::$lint_name, + $level, + $desc, + report_in_external_macro:true + $(, @eval_always = $eval_always)? + } + + pub(crate) static ${concat($lint_name, _INFO)}: &'static $crate::LintInfo = &$crate::LintInfo { + lint: $lint_name, + category: $crate::LintCategory::$category, + explanation: concat!($($docs,"\n",)*), + location: concat!(file!(), "#L", line!()), + version: $version, + }; + }; +} + +#[macro_export] +macro_rules! declare_clippy_lint { + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + correctness, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Deny, + Correctness, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + complexity, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Warn, + Complexity, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + perf, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Warn, + Perf, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + style, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Warn, + Style, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + suspicious, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Warn, + Suspicious, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + cargo, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Allow, + Cargo, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + nursery, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Allow, + Nursery, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + pedantic, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Allow, + Pedantic, + $($rest)* + } + }; + ( + $(#[$($meta:tt)*])* + $vis:vis $lint_name:ident, + restriction, + $($rest:tt)* + ) => { + $crate::declare_clippy_lint_inner! { + $(#[$($meta)*])* + $vis $lint_name, + Allow, + Restriction, + $($rest)* + } + }; +} diff --git a/src/tools/clippy/lintcheck/src/main.rs b/src/tools/clippy/lintcheck/src/main.rs index 84183831432..eb390eecbcc 100644 --- a/src/tools/clippy/lintcheck/src/main.rs +++ b/src/tools/clippy/lintcheck/src/main.rs @@ -45,7 +45,7 @@ use rayon::prelude::*; #[must_use] pub fn target_dir() -> String { - env::var("CARGO_TARGET_DIR").unwrap_or("target".to_owned()) + env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_owned()) } fn lintcheck_sources() -> String { diff --git a/src/tools/clippy/rust-toolchain.toml b/src/tools/clippy/rust-toolchain.toml index 3fc5a1224a8..124756a3600 100644 --- a/src/tools/clippy/rust-toolchain.toml +++ b/src/tools/clippy/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # begin autogenerated nightly -channel = "nightly-2025-06-12" +channel = "nightly-2025-06-26" # end autogenerated nightly components = ["cargo", "llvm-tools", "rust-src", "rust-std", "rustc", "rustc-dev", "rustfmt"] profile = "minimal" diff --git a/src/tools/clippy/src/driver.rs b/src/tools/clippy/src/driver.rs index 426ba870f5f..c4076cbaa77 100644 --- a/src/tools/clippy/src/driver.rs +++ b/src/tools/clippy/src/driver.rs @@ -19,6 +19,7 @@ extern crate rustc_span; extern crate tikv_jemalloc_sys as jemalloc_sys; use clippy_utils::sym; +use declare_clippy_lint::LintListBuilder; use rustc_interface::interface; use rustc_session::EarlyDiagCtxt; use rustc_session::config::ErrorOutputType; @@ -156,8 +157,13 @@ impl rustc_driver::Callbacks for ClippyCallbacks { (previous)(sess, lint_store); } + let mut list_builder = LintListBuilder::default(); + list_builder.insert(clippy_lints::declared_lints::LINTS); + list_builder.register(lint_store); + let conf = clippy_config::Conf::read(sess, &conf_path); - clippy_lints::register_lints(lint_store, conf); + clippy_lints::register_lint_passes(lint_store, conf); + #[cfg(feature = "internal")] clippy_lints_internal::register_lints(lint_store); })); diff --git a/src/tools/clippy/src/main.rs b/src/tools/clippy/src/main.rs index c9853e53f3b..3c2eec1f05b 100644 --- a/src/tools/clippy/src/main.rs +++ b/src/tools/clippy/src/main.rs @@ -107,7 +107,7 @@ impl ClippyCmd { } fn into_std_cmd(self) -> Command { - let mut cmd = Command::new(env::var("CARGO").unwrap_or("cargo".into())); + let mut cmd = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".into())); let clippy_args: String = self .clippy_args .iter() diff --git a/src/tools/clippy/tests/compile-test.rs b/src/tools/clippy/tests/compile-test.rs index 99a01257a7b..cefe654fef6 100644 --- a/src/tools/clippy/tests/compile-test.rs +++ b/src/tools/clippy/tests/compile-test.rs @@ -7,9 +7,9 @@ use askama::filters::Safe; use cargo_metadata::Message; use cargo_metadata::diagnostic::{Applicability, Diagnostic}; use clippy_config::ClippyConfiguration; -use clippy_lints::LintInfo; use clippy_lints::declared_lints::LINTS; use clippy_lints::deprecated_lints::{DEPRECATED, DEPRECATED_VERSION, RENAMED}; +use declare_clippy_lint::LintInfo; use pulldown_cmark::{Options, Parser, html}; use serde::Deserialize; use test_utils::IS_RUSTC_TEST_SUITE; @@ -568,10 +568,10 @@ impl LintMetadata { Self { id: name, id_location: Some(lint.location), - group: lint.category_str(), + group: lint.category.name(), level: lint.lint.default_level.as_str(), docs, - version: lint.version.unwrap(), + version: lint.version, applicability, } } diff --git a/src/tools/clippy/tests/dogfood.rs b/src/tools/clippy/tests/dogfood.rs index 4ac2bd53285..389616801fc 100644 --- a/src/tools/clippy/tests/dogfood.rs +++ b/src/tools/clippy/tests/dogfood.rs @@ -40,6 +40,7 @@ fn dogfood() { "clippy_lints", "clippy_utils", "clippy_config", + "declare_clippy_lint", "lintcheck", "rustc_tools_util", ] { diff --git a/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.fixed b/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.fixed new file mode 100644 index 00000000000..0dc0fc230c8 --- /dev/null +++ b/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.fixed @@ -0,0 +1,50 @@ +#![allow(clippy::eq_op, clippy::nonminimal_bool)] + +#[rustfmt::skip] +#[warn(clippy::collapsible_if)] +fn main() { + let (x, y) = ("hello", "world"); + + if x == "hello" { + todo!() + } + // Comment must be kept + else if y == "world" { + println!("Hello world!"); + } + //~^^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } // Inner comment + else if y == "world" { + println!("Hello world!"); + } + //~^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } + /* Inner comment */ + else if y == "world" { + println!("Hello world!"); + } + //~^^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } /* Inner comment */ + else if y == "world" { + println!("Hello world!"); + } + //~^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } /* This should not be removed */ /* So does this */ + // Comment must be kept + else if y == "world" { + println!("Hello world!"); + } + //~^^^^^^ collapsible_else_if +} diff --git a/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.rs b/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.rs new file mode 100644 index 00000000000..8344c122f16 --- /dev/null +++ b/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.rs @@ -0,0 +1,55 @@ +#![allow(clippy::eq_op, clippy::nonminimal_bool)] + +#[rustfmt::skip] +#[warn(clippy::collapsible_if)] +fn main() { + let (x, y) = ("hello", "world"); + + if x == "hello" { + todo!() + } else { + // Comment must be kept + if y == "world" { + println!("Hello world!"); + } + } + //~^^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } else { // Inner comment + if y == "world" { + println!("Hello world!"); + } + } + //~^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } else { + /* Inner comment */ + if y == "world" { + println!("Hello world!"); + } + } + //~^^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } else { /* Inner comment */ + if y == "world" { + println!("Hello world!"); + } + } + //~^^^^^ collapsible_else_if + + if x == "hello" { + todo!() + } /* This should not be removed */ else /* So does this */ { + // Comment must be kept + if y == "world" { + println!("Hello world!"); + } + } + //~^^^^^^ collapsible_else_if +} diff --git a/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.stderr b/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.stderr new file mode 100644 index 00000000000..0ffe5f0a960 --- /dev/null +++ b/src/tools/clippy/tests/ui-toml/collapsible_if/collapsible_else_if.stderr @@ -0,0 +1,105 @@ +error: this `else { if .. }` block can be collapsed + --> tests/ui-toml/collapsible_if/collapsible_else_if.rs:10:12 + | +LL | } else { + | ____________^ +LL | | // Comment must be kept +LL | | if y == "world" { +LL | | println!("Hello world!"); +LL | | } +LL | | } + | |_____^ + | + = note: `-D clippy::collapsible-else-if` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::collapsible_else_if)]` +help: collapse nested if block + | +LL ~ } +LL | // Comment must be kept +LL ~ else if y == "world" { +LL | println!("Hello world!"); +LL ~ } + | + +error: this `else { if .. }` block can be collapsed + --> tests/ui-toml/collapsible_if/collapsible_else_if.rs:20:12 + | +LL | } else { // Inner comment + | ____________^ +LL | | if y == "world" { +LL | | println!("Hello world!"); +LL | | } +LL | | } + | |_____^ + | +help: collapse nested if block + | +LL ~ } // Inner comment +LL ~ else if y == "world" { +LL | println!("Hello world!"); +LL ~ } + | + +error: this `else { if .. }` block can be collapsed + --> tests/ui-toml/collapsible_if/collapsible_else_if.rs:29:12 + | +LL | } else { + | ____________^ +LL | | /* Inner comment */ +LL | | if y == "world" { +LL | | println!("Hello world!"); +LL | | } +LL | | } + | |_____^ + | +help: collapse nested if block + | +LL ~ } +LL | /* Inner comment */ +LL ~ else if y == "world" { +LL | println!("Hello world!"); +LL ~ } + | + +error: this `else { if .. }` block can be collapsed + --> tests/ui-toml/collapsible_if/collapsible_else_if.rs:39:12 + | +LL | } else { /* Inner comment */ + | ____________^ +LL | | if y == "world" { +LL | | println!("Hello world!"); +LL | | } +LL | | } + | |_____^ + | +help: collapse nested if block + | +LL ~ } /* Inner comment */ +LL ~ else if y == "world" { +LL | println!("Hello world!"); +LL ~ } + | + +error: this `else { if .. }` block can be collapsed + --> tests/ui-toml/collapsible_if/collapsible_else_if.rs:48:64 + | +LL | } /* This should not be removed */ else /* So does this */ { + | ________________________________________________________________^ +LL | | // Comment must be kept +LL | | if y == "world" { +LL | | println!("Hello world!"); +LL | | } +LL | | } + | |_____^ + | +help: collapse nested if block + | +LL ~ } /* This should not be removed */ /* So does this */ +LL | // Comment must be kept +LL ~ else if y == "world" { +LL | println!("Hello world!"); +LL ~ } + | + +error: aborting due to 5 previous errors + diff --git a/src/tools/clippy/tests/ui/assign_ops.fixed b/src/tools/clippy/tests/ui/assign_ops.fixed index 3bc6885d7c3..99beea850a2 100644 --- a/src/tools/clippy/tests/ui/assign_ops.fixed +++ b/src/tools/clippy/tests/ui/assign_ops.fixed @@ -91,7 +91,7 @@ mod issue14871 { impl<T> const NumberConstants for T where - T: Number + ~const core::ops::Add, + T: Number + [const] core::ops::Add, { fn constant(value: usize) -> Self { let mut res = Self::ZERO; diff --git a/src/tools/clippy/tests/ui/assign_ops.rs b/src/tools/clippy/tests/ui/assign_ops.rs index f1f8f9daff9..900d5ad38e0 100644 --- a/src/tools/clippy/tests/ui/assign_ops.rs +++ b/src/tools/clippy/tests/ui/assign_ops.rs @@ -91,7 +91,7 @@ mod issue14871 { impl<T> const NumberConstants for T where - T: Number + ~const core::ops::Add, + T: Number + [const] core::ops::Add, { fn constant(value: usize) -> Self { let mut res = Self::ZERO; diff --git a/src/tools/clippy/tests/ui/borrow_deref_ref.fixed b/src/tools/clippy/tests/ui/borrow_deref_ref.fixed index 765dd75fceb..6d06fcc3037 100644 --- a/src/tools/clippy/tests/ui/borrow_deref_ref.fixed +++ b/src/tools/clippy/tests/ui/borrow_deref_ref.fixed @@ -124,3 +124,50 @@ mod issue_11346 { //~^ borrow_deref_ref } } + +fn issue_14934() { + let x: &'static str = "x"; + let y = "y".to_string(); + { + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*x; // Do not lint + *x = &*y; + } + { + let mut x = x; + //~^ borrow_deref_ref + x = &*y; + } + { + #[expect(clippy::toplevel_ref_arg, clippy::needless_borrow)] + let ref x = x; + //~^ borrow_deref_ref + } + { + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = std::convert::identity(x); + //~^ borrow_deref_ref + *x = &*y; + } + { + #[derive(Clone)] + struct S(&'static str); + let s = S("foo"); + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*s.0; // Do not lint + *x = "bar"; + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = s.clone().0; + //~^ borrow_deref_ref + *x = "bar"; + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*std::convert::identity(&s).0; + *x = "bar"; + } + { + let y = &1; + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = { y }; + //~^ borrow_deref_ref + } +} diff --git a/src/tools/clippy/tests/ui/borrow_deref_ref.rs b/src/tools/clippy/tests/ui/borrow_deref_ref.rs index 8ee66bfa881..b43f4c93bf2 100644 --- a/src/tools/clippy/tests/ui/borrow_deref_ref.rs +++ b/src/tools/clippy/tests/ui/borrow_deref_ref.rs @@ -124,3 +124,50 @@ mod issue_11346 { //~^ borrow_deref_ref } } + +fn issue_14934() { + let x: &'static str = "x"; + let y = "y".to_string(); + { + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*x; // Do not lint + *x = &*y; + } + { + let mut x = &*x; + //~^ borrow_deref_ref + x = &*y; + } + { + #[expect(clippy::toplevel_ref_arg, clippy::needless_borrow)] + let ref x = &*x; + //~^ borrow_deref_ref + } + { + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*std::convert::identity(x); + //~^ borrow_deref_ref + *x = &*y; + } + { + #[derive(Clone)] + struct S(&'static str); + let s = S("foo"); + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*s.0; // Do not lint + *x = "bar"; + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*s.clone().0; + //~^ borrow_deref_ref + *x = "bar"; + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = &*std::convert::identity(&s).0; + *x = "bar"; + } + { + let y = &1; + #[expect(clippy::toplevel_ref_arg)] + let ref mut x = { &*y }; + //~^ borrow_deref_ref + } +} diff --git a/src/tools/clippy/tests/ui/borrow_deref_ref.stderr b/src/tools/clippy/tests/ui/borrow_deref_ref.stderr index 3d55da25b9b..3a1f968b4be 100644 --- a/src/tools/clippy/tests/ui/borrow_deref_ref.stderr +++ b/src/tools/clippy/tests/ui/borrow_deref_ref.stderr @@ -25,5 +25,35 @@ error: deref on an immutable reference LL | (&*s).foo(); | ^^^^^ help: if you would like to reborrow, try removing `&*`: `s` -error: aborting due to 4 previous errors +error: deref on an immutable reference + --> tests/ui/borrow_deref_ref.rs:137:21 + | +LL | let mut x = &*x; + | ^^^ help: if you would like to reborrow, try removing `&*`: `x` + +error: deref on an immutable reference + --> tests/ui/borrow_deref_ref.rs:143:21 + | +LL | let ref x = &*x; + | ^^^ help: if you would like to reborrow, try removing `&*`: `x` + +error: deref on an immutable reference + --> tests/ui/borrow_deref_ref.rs:148:25 + | +LL | let ref mut x = &*std::convert::identity(x); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: if you would like to reborrow, try removing `&*`: `std::convert::identity(x)` + +error: deref on an immutable reference + --> tests/ui/borrow_deref_ref.rs:160:25 + | +LL | let ref mut x = &*s.clone().0; + | ^^^^^^^^^^^^^ help: if you would like to reborrow, try removing `&*`: `s.clone().0` + +error: deref on an immutable reference + --> tests/ui/borrow_deref_ref.rs:170:27 + | +LL | let ref mut x = { &*y }; + | ^^^ help: if you would like to reborrow, try removing `&*`: `y` + +error: aborting due to 9 previous errors diff --git a/src/tools/clippy/tests/ui/borrow_interior_mutable_const.rs b/src/tools/clippy/tests/ui/borrow_interior_mutable_const.rs index 0f439f78915..674450a73ad 100644 --- a/src/tools/clippy/tests/ui/borrow_interior_mutable_const.rs +++ b/src/tools/clippy/tests/ui/borrow_interior_mutable_const.rs @@ -218,4 +218,20 @@ fn main() { let _ = &S::VALUE.1; //~ borrow_interior_mutable_const let _ = &S::VALUE.2; } + { + pub struct Foo<T, const N: usize>(pub Entry<N>, pub T); + + pub struct Entry<const N: usize>(pub Cell<[u32; N]>); + + impl<const N: usize> Entry<N> { + const INIT: Self = Self(Cell::new([42; N])); + } + + impl<T, const N: usize> Foo<T, N> { + pub fn make_foo(v: T) -> Self { + // Used to ICE due to incorrect instantiation. + Foo(Entry::INIT, v) + } + } + } } diff --git a/src/tools/clippy/tests/ui/box_default.fixed b/src/tools/clippy/tests/ui/box_default.fixed index 80000f5de4f..ed00494433b 100644 --- a/src/tools/clippy/tests/ui/box_default.fixed +++ b/src/tools/clippy/tests/ui/box_default.fixed @@ -126,7 +126,7 @@ fn issue_10381() { impl Bar for Foo {} fn maybe_get_bar(i: u32) -> Option<Box<dyn Bar>> { - if i % 2 == 0 { + if i.is_multiple_of(2) { Some(Box::new(Foo::default())) } else { None diff --git a/src/tools/clippy/tests/ui/box_default.rs b/src/tools/clippy/tests/ui/box_default.rs index 4681016d7cd..801d92f5c29 100644 --- a/src/tools/clippy/tests/ui/box_default.rs +++ b/src/tools/clippy/tests/ui/box_default.rs @@ -126,7 +126,7 @@ fn issue_10381() { impl Bar for Foo {} fn maybe_get_bar(i: u32) -> Option<Box<dyn Bar>> { - if i % 2 == 0 { + if i.is_multiple_of(2) { Some(Box::new(Foo::default())) } else { None diff --git a/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.rs b/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.rs index 922d30443fc..fa322dc28a7 100644 --- a/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.rs +++ b/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.rs @@ -276,3 +276,27 @@ mod issue14873 { } } } + +fn issue15004() { + let a = 12u32; + let b = 13u32; + let mut c = 8u32; + + let mut result = if b > a { + c += 1; + 0 + } else { + c += 2; + 0 + //~^ branches_sharing_code + }; + + result = if b > a { + c += 1; + 1 + } else { + c += 2; + 1 + //~^ branches_sharing_code + }; +} diff --git a/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.stderr b/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.stderr index f437db8b733..1c470fb0da5 100644 --- a/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.stderr +++ b/src/tools/clippy/tests/ui/branches_sharing_code/shared_at_bottom.stderr @@ -172,5 +172,35 @@ LL ~ } LL + let y = 1; | -error: aborting due to 10 previous errors +error: all if blocks contain the same code at the end + --> tests/ui/branches_sharing_code/shared_at_bottom.rs:290:5 + | +LL | / 0 +LL | | +LL | | }; + | |_____^ + | + = note: the end suggestion probably needs some adjustments to use the expression result correctly +help: consider moving these statements after the if + | +LL ~ } +LL ~ 0; + | + +error: all if blocks contain the same code at the end + --> tests/ui/branches_sharing_code/shared_at_bottom.rs:299:5 + | +LL | / 1 +LL | | +LL | | }; + | |_____^ + | + = note: the end suggestion probably needs some adjustments to use the expression result correctly +help: consider moving these statements after the if + | +LL ~ } +LL ~ 1; + | + +error: aborting due to 12 previous errors diff --git a/src/tools/clippy/tests/ui/collapsible_else_if.fixed b/src/tools/clippy/tests/ui/collapsible_else_if.fixed index 9f530ad670a..fed75244c6f 100644 --- a/src/tools/clippy/tests/ui/collapsible_else_if.fixed +++ b/src/tools/clippy/tests/ui/collapsible_else_if.fixed @@ -86,3 +86,21 @@ fn issue_7318() { }else if false {} //~^^^ collapsible_else_if } + +fn issue14799() { + use std::ops::ControlFlow; + + let c: ControlFlow<_, ()> = ControlFlow::Break(Some(42)); + if let ControlFlow::Break(Some(_)) = c { + todo!(); + } else { + #[cfg(target_os = "freebsd")] + todo!(); + + if let ControlFlow::Break(None) = c { + todo!(); + } else { + todo!(); + } + } +} diff --git a/src/tools/clippy/tests/ui/collapsible_else_if.rs b/src/tools/clippy/tests/ui/collapsible_else_if.rs index 2c646cd1d4d..e50e781fb69 100644 --- a/src/tools/clippy/tests/ui/collapsible_else_if.rs +++ b/src/tools/clippy/tests/ui/collapsible_else_if.rs @@ -102,3 +102,21 @@ fn issue_7318() { } //~^^^ collapsible_else_if } + +fn issue14799() { + use std::ops::ControlFlow; + + let c: ControlFlow<_, ()> = ControlFlow::Break(Some(42)); + if let ControlFlow::Break(Some(_)) = c { + todo!(); + } else { + #[cfg(target_os = "freebsd")] + todo!(); + + if let ControlFlow::Break(None) = c { + todo!(); + } else { + todo!(); + } + } +} diff --git a/src/tools/clippy/tests/ui/collapsible_if.fixed b/src/tools/clippy/tests/ui/collapsible_if.fixed index b553182a445..77bc791ea8e 100644 --- a/src/tools/clippy/tests/ui/collapsible_if.fixed +++ b/src/tools/clippy/tests/ui/collapsible_if.fixed @@ -154,3 +154,12 @@ fn issue14722() { None }; } + +fn issue14799() { + if true { + #[cfg(target_os = "freebsd")] + todo!(); + + if true {} + }; +} diff --git a/src/tools/clippy/tests/ui/collapsible_if.rs b/src/tools/clippy/tests/ui/collapsible_if.rs index f5998457ca6..d30df157d5e 100644 --- a/src/tools/clippy/tests/ui/collapsible_if.rs +++ b/src/tools/clippy/tests/ui/collapsible_if.rs @@ -164,3 +164,12 @@ fn issue14722() { None }; } + +fn issue14799() { + if true { + #[cfg(target_os = "freebsd")] + todo!(); + + if true {} + }; +} diff --git a/src/tools/clippy/tests/ui/doc/needless_doctest_main.rs b/src/tools/clippy/tests/ui/doc/needless_doctest_main.rs index 633a435ca5e..8c3217624d4 100644 --- a/src/tools/clippy/tests/ui/doc/needless_doctest_main.rs +++ b/src/tools/clippy/tests/ui/doc/needless_doctest_main.rs @@ -1,5 +1,3 @@ -//@ check-pass - #![warn(clippy::needless_doctest_main)] //! issue 10491: //! ```rust,no_test @@ -19,4 +17,114 @@ /// ``` fn foo() {} +#[rustfmt::skip] +/// Description +/// ```rust +/// fn main() { +//~^ error: needless `fn main` in doctest +/// let a = 0; +/// } +/// ``` +fn mulpipulpi() {} + +#[rustfmt::skip] +/// With a `#[no_main]` +/// ```rust +/// #[no_main] +/// fn a() { +/// let _ = 0; +/// } +/// ``` +fn pulpimulpi() {} + +// Without a `#[no_main]` attribute +/// ```rust +/// fn a() { +/// let _ = 0; +/// } +/// ``` +fn plumilupi() {} + +#[rustfmt::skip] +/// Additional function, shouldn't trigger +/// ```rust +/// fn additional_function() { +/// let _ = 0; +/// // Thus `fn main` is actually relevant! +/// } +/// fn main() { +/// let _ = 0; +/// } +/// ``` +fn mlupipupi() {} + +#[rustfmt::skip] +/// Additional function AFTER main, shouldn't trigger +/// ```rust +/// fn main() { +/// let _ = 0; +/// } +/// fn additional_function() { +/// let _ = 0; +/// // Thus `fn main` is actually relevant! +/// } +/// ``` +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 +/// let _ = 0; +/// } +/// ``` +fn plumpiplupi() {} + +/// 4 Functions, this should not lint because there are several function +/// +/// ```rust +/// fn a() {let _ = 0; } +/// fn b() {let _ = 0; } +/// fn main() { let _ = 0; } +/// fn d() { let _ = 0; } +/// ``` +fn pulmipulmip() {} + +/// 3 Functions but main is first, should also not lint +/// +///```rust +/// fn main() { let _ = 0; } +/// fn b() { let _ = 0; } +/// fn c() { let _ = 0; } +/// ``` +fn pmuplimulip() {} + fn main() {} + +fn issue8244() -> Result<(), ()> { + //! ```compile_fail + //! fn test() -> Result< {} + //! ``` + Ok(()) +} + +/// # Examples +/// +/// ``` +/// use std::error::Error; +/// fn main() -> Result<(), Box<dyn Error>/* > */ { +/// } +/// ``` +fn issue15041() {} diff --git a/src/tools/clippy/tests/ui/doc/needless_doctest_main.stderr b/src/tools/clippy/tests/ui/doc/needless_doctest_main.stderr new file mode 100644 index 00000000000..dd5474ccb85 --- /dev/null +++ b/src/tools/clippy/tests/ui/doc/needless_doctest_main.stderr @@ -0,0 +1,36 @@ +error: needless `fn main` in doctest + --> tests/ui/doc/needless_doctest_main.rs:23:5 + | +LL | /// fn main() { + | _____^ +LL | | +LL | | /// let a = 0; +LL | | /// } + | |_____^ + | + = 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 + | +LL | /// fn main() { + | _____^ +LL | | +LL | | /// // Hi! +LL | | /// let _ = 0; +LL | | /// } + | |_____^ + +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 + diff --git a/src/tools/clippy/tests/ui/doc_broken_link.rs b/src/tools/clippy/tests/ui/doc_broken_link.rs new file mode 100644 index 00000000000..7d9c0ef13b3 --- /dev/null +++ b/src/tools/clippy/tests/ui/doc_broken_link.rs @@ -0,0 +1,72 @@ +#![warn(clippy::doc_broken_link)] + +fn main() {} + +pub struct FakeType {} + +/// This might be considered a link false positive +/// and should be ignored by this lint rule: +/// Example of referencing some code with brackets [FakeType]. +pub fn doc_ignore_link_false_positive_1() {} + +/// This might be considered a link false positive +/// and should be ignored by this lint rule: +/// [`FakeType`]. Continue text after brackets, +/// then (something in +/// parenthesis). +pub fn doc_ignore_link_false_positive_2() {} + +/// Test valid link, whole link single line. +/// [doc valid link](https://test.fake/doc_valid_link) +pub fn doc_valid_link() {} + +/// Test valid link, whole link single line but it has special chars such as brackets and +/// parenthesis. [doc invalid link url invalid char](https://test.fake/doc_valid_link_url_invalid_char?foo[bar]=1&bar(foo)=2) +pub fn doc_valid_link_url_invalid_char() {} + +/// Test valid link, text tag broken across multiple lines. +/// [doc valid link broken +/// text](https://test.fake/doc_valid_link_broken_text) +pub fn doc_valid_link_broken_text() {} + +/// Test valid link, url tag broken across multiple lines, but +/// the whole url part in a single line. +/// [doc valid link broken url tag two lines first](https://test.fake/doc_valid_link_broken_url_tag_two_lines_first +/// ) +pub fn doc_valid_link_broken_url_tag_two_lines_first() {} + +/// Test valid link, url tag broken across multiple lines, but +/// the whole url part in a single line. +/// [doc valid link broken url tag two lines second]( +/// https://test.fake/doc_valid_link_broken_url_tag_two_lines_second) +pub fn doc_valid_link_broken_url_tag_two_lines_second() {} + +/// Test valid link, url tag broken across multiple lines, but +/// the whole url part in a single line, but the closing pharentesis +/// in a third line. +/// [doc valid link broken url tag three lines]( +/// https://test.fake/doc_valid_link_broken_url_tag_three_lines +/// ) +pub fn doc_valid_link_broken_url_tag_three_lines() {} + +/// Test invalid link, url part broken across multiple lines. +/// [doc invalid link broken url scheme part](https:// +/// test.fake/doc_invalid_link_broken_url_scheme_part) +//~^^ ERROR: possible broken doc link: broken across multiple lines +pub fn doc_invalid_link_broken_url_scheme_part() {} + +/// Test invalid link, url part broken across multiple lines. +/// [doc invalid link broken url host part](https://test +/// .fake/doc_invalid_link_broken_url_host_part) +//~^^ ERROR: possible broken doc link: broken across multiple lines +pub fn doc_invalid_link_broken_url_host_part() {} + +/// Test invalid link, for multiple urls in the same block of comment. +/// There is a [fist link - invalid](https://test +/// .fake) then it continues +//~^^ ERROR: possible broken doc link: broken across multiple lines +/// with a [second link - valid](https://test.fake/doc_valid_link) and another [third link - invalid](https://test +/// .fake). It ends with another +//~^^ ERROR: possible broken doc link: broken across multiple lines +/// line of comment. +pub fn doc_multiple_invalid_link_broken_url() {} diff --git a/src/tools/clippy/tests/ui/doc_broken_link.stderr b/src/tools/clippy/tests/ui/doc_broken_link.stderr new file mode 100644 index 00000000000..179ed97635e --- /dev/null +++ b/src/tools/clippy/tests/ui/doc_broken_link.stderr @@ -0,0 +1,29 @@ +error: possible broken doc link: broken across multiple lines + --> tests/ui/doc_broken_link.rs:53:5 + | +LL | /// [doc invalid link broken url scheme part](https:// + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `-D clippy::doc-broken-link` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::doc_broken_link)]` + +error: possible broken doc link: broken across multiple lines + --> tests/ui/doc_broken_link.rs:59:5 + | +LL | /// [doc invalid link broken url host part](https://test + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: possible broken doc link: broken across multiple lines + --> tests/ui/doc_broken_link.rs:65:16 + | +LL | /// There is a [fist link - invalid](https://test + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: possible broken doc link: broken across multiple lines + --> tests/ui/doc_broken_link.rs:68:80 + | +LL | /// with a [second link - valid](https://test.fake/doc_valid_link) and another [third link - invalid](https://test + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 4 previous errors + diff --git a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.1.fixed b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.1.fixed index 36d80a2c95b..e36e3c2aea6 100644 --- a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.1.fixed +++ b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.1.fixed @@ -105,4 +105,13 @@ second line ")] pub struct Args; +mod issue_14980 { + //~v empty_line_after_outer_attr + #[repr(align(536870912))] + enum Aligned { + Zero = 0, + One = 1, + } +} + fn main() {} diff --git a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.2.fixed b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.2.fixed index 0e8e4129e85..b0908fc7214 100644 --- a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.2.fixed +++ b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.2.fixed @@ -108,4 +108,13 @@ second line ")] pub struct Args; +mod issue_14980 { + //~v empty_line_after_outer_attr + #[repr(align(536870912))] + enum Aligned { + Zero = 0, + One = 1, + } +} + fn main() {} diff --git a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.rs b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.rs index 1295088ac00..4ae113c68f5 100644 --- a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.rs +++ b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.rs @@ -116,4 +116,14 @@ second line ")] pub struct Args; +mod issue_14980 { + //~v empty_line_after_outer_attr + #[repr(align(536870912))] + + enum Aligned { + Zero = 0, + One = 1, + } +} + fn main() {} diff --git a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr index 519ba6e6761..331bc7c8856 100644 --- a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr +++ b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr @@ -111,5 +111,16 @@ LL | pub fn isolated_comment() {} | = help: if the empty lines are unintentional, remove them -error: aborting due to 9 previous errors +error: empty line after outer attribute + --> tests/ui/empty_line_after/outer_attribute.rs:121:5 + | +LL | / #[repr(align(536870912))] +LL | | + | |_^ +LL | enum Aligned { + | ------------ the attribute applies to this enum + | + = help: if the empty line is unintentional, remove it + +error: aborting due to 10 previous errors diff --git a/src/tools/clippy/tests/ui/eta.fixed b/src/tools/clippy/tests/ui/eta.fixed index 0ba631fda05..c93b83f53ec 100644 --- a/src/tools/clippy/tests/ui/eta.fixed +++ b/src/tools/clippy/tests/ui/eta.fixed @@ -543,3 +543,21 @@ mod issue_13073 { //~^ redundant_closure } } + +fn issue_14789() { + _ = Some(1u8).map( + #[expect(clippy::redundant_closure)] + |a| foo(a), + ); + + _ = Some("foo").map( + #[expect(clippy::redundant_closure_for_method_calls)] + |s| s.to_owned(), + ); + + let _: Vec<u8> = None.map_or_else( + #[expect(clippy::redundant_closure)] + || vec![], + std::convert::identity, + ); +} diff --git a/src/tools/clippy/tests/ui/eta.rs b/src/tools/clippy/tests/ui/eta.rs index 4d8b29d450c..273c8b21f4a 100644 --- a/src/tools/clippy/tests/ui/eta.rs +++ b/src/tools/clippy/tests/ui/eta.rs @@ -543,3 +543,21 @@ mod issue_13073 { //~^ redundant_closure } } + +fn issue_14789() { + _ = Some(1u8).map( + #[expect(clippy::redundant_closure)] + |a| foo(a), + ); + + _ = Some("foo").map( + #[expect(clippy::redundant_closure_for_method_calls)] + |s| s.to_owned(), + ); + + let _: Vec<u8> = None.map_or_else( + #[expect(clippy::redundant_closure)] + || vec![], + std::convert::identity, + ); +} diff --git a/src/tools/clippy/tests/ui/exhaustive_items.fixed b/src/tools/clippy/tests/ui/exhaustive_items.fixed index 79c74aeefbd..3b2f33dbd2c 100644 --- a/src/tools/clippy/tests/ui/exhaustive_items.fixed +++ b/src/tools/clippy/tests/ui/exhaustive_items.fixed @@ -1,3 +1,4 @@ +#![feature(default_field_values)] #![deny(clippy::exhaustive_enums, clippy::exhaustive_structs)] #![allow(unused)] @@ -90,3 +91,9 @@ pub mod structs { pub bar: String, } } + +pub mod issue14992 { + pub struct A { + pub a: isize = 42, + } +} diff --git a/src/tools/clippy/tests/ui/exhaustive_items.rs b/src/tools/clippy/tests/ui/exhaustive_items.rs index 4e851f4c492..b0a6a717076 100644 --- a/src/tools/clippy/tests/ui/exhaustive_items.rs +++ b/src/tools/clippy/tests/ui/exhaustive_items.rs @@ -1,3 +1,4 @@ +#![feature(default_field_values)] #![deny(clippy::exhaustive_enums, clippy::exhaustive_structs)] #![allow(unused)] @@ -87,3 +88,9 @@ pub mod structs { pub bar: String, } } + +pub mod issue14992 { + pub struct A { + pub a: isize = 42, + } +} diff --git a/src/tools/clippy/tests/ui/exhaustive_items.stderr b/src/tools/clippy/tests/ui/exhaustive_items.stderr index c92c8a9efaa..55928fa458d 100644 --- a/src/tools/clippy/tests/ui/exhaustive_items.stderr +++ b/src/tools/clippy/tests/ui/exhaustive_items.stderr @@ -1,5 +1,5 @@ error: exported enums should not be exhaustive - --> tests/ui/exhaustive_items.rs:9:5 + --> tests/ui/exhaustive_items.rs:10:5 | LL | / pub enum Exhaustive { LL | | @@ -11,7 +11,7 @@ LL | | } | |_____^ | note: the lint level is defined here - --> tests/ui/exhaustive_items.rs:1:9 + --> tests/ui/exhaustive_items.rs:2:9 | LL | #![deny(clippy::exhaustive_enums, clippy::exhaustive_structs)] | ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -22,7 +22,7 @@ LL ~ pub enum Exhaustive { | error: exported enums should not be exhaustive - --> tests/ui/exhaustive_items.rs:19:5 + --> tests/ui/exhaustive_items.rs:20:5 | LL | / pub enum ExhaustiveWithAttrs { LL | | @@ -40,7 +40,7 @@ LL ~ pub enum ExhaustiveWithAttrs { | error: exported structs should not be exhaustive - --> tests/ui/exhaustive_items.rs:55:5 + --> tests/ui/exhaustive_items.rs:56:5 | LL | / pub struct Exhaustive { LL | | @@ -50,7 +50,7 @@ LL | | } | |_____^ | note: the lint level is defined here - --> tests/ui/exhaustive_items.rs:1:35 + --> tests/ui/exhaustive_items.rs:2:35 | LL | #![deny(clippy::exhaustive_enums, clippy::exhaustive_structs)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/tools/clippy/tests/ui/identity_op.fixed b/src/tools/clippy/tests/ui/identity_op.fixed index a1b55602998..4e14e1a5e33 100644 --- a/src/tools/clippy/tests/ui/identity_op.fixed +++ b/src/tools/clippy/tests/ui/identity_op.fixed @@ -312,3 +312,49 @@ fn issue_13470() { let _: u64 = 1u64 + ((x as i32 + y as i32) as u64); //~^ identity_op } + +fn issue_14932() { + let _ = 0usize + &Default::default(); // no error + + 0usize + &Default::default(); // no error + + <usize as Default>::default(); + //~^ identity_op + + let _ = usize::default(); + //~^ identity_op + + let _n: usize = Default::default(); + //~^ identity_op +} + +// Expr's type can be inferred by the function's return type +fn issue_14932_2() -> usize { + Default::default() + //~^ identity_op +} + +trait Def { + fn def() -> Self; +} + +impl Def for usize { + fn def() -> Self { + 0 + } +} + +fn issue_14932_3() { + let _ = 0usize + &Def::def(); // no error + + 0usize + &Def::def(); // no error + + <usize as Def>::def(); + //~^ identity_op + + let _ = usize::def(); + //~^ identity_op + + let _n: usize = Def::def(); + //~^ identity_op +} diff --git a/src/tools/clippy/tests/ui/identity_op.rs b/src/tools/clippy/tests/ui/identity_op.rs index f603e1078e4..ebbef5723ff 100644 --- a/src/tools/clippy/tests/ui/identity_op.rs +++ b/src/tools/clippy/tests/ui/identity_op.rs @@ -312,3 +312,49 @@ fn issue_13470() { let _: u64 = 1u64 + ((x as i32 + y as i32) as u64 + 0u64); //~^ identity_op } + +fn issue_14932() { + let _ = 0usize + &Default::default(); // no error + + 0usize + &Default::default(); // no error + + 0usize + &<usize as Default>::default(); + //~^ identity_op + + let _ = 0usize + &usize::default(); + //~^ identity_op + + let _n: usize = 0usize + &Default::default(); + //~^ identity_op +} + +// Expr's type can be inferred by the function's return type +fn issue_14932_2() -> usize { + 0usize + &Default::default() + //~^ identity_op +} + +trait Def { + fn def() -> Self; +} + +impl Def for usize { + fn def() -> Self { + 0 + } +} + +fn issue_14932_3() { + let _ = 0usize + &Def::def(); // no error + + 0usize + &Def::def(); // no error + + 0usize + &<usize as Def>::def(); + //~^ identity_op + + let _ = 0usize + &usize::def(); + //~^ identity_op + + let _n: usize = 0usize + &Def::def(); + //~^ identity_op +} diff --git a/src/tools/clippy/tests/ui/identity_op.stderr b/src/tools/clippy/tests/ui/identity_op.stderr index 8f9c2b603c4..24fa5db08ce 100644 --- a/src/tools/clippy/tests/ui/identity_op.stderr +++ b/src/tools/clippy/tests/ui/identity_op.stderr @@ -379,5 +379,47 @@ error: this operation has no effect LL | let _: u64 = 1u64 + ((x as i32 + y as i32) as u64 + 0u64); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `((x as i32 + y as i32) as u64)` -error: aborting due to 63 previous errors +error: this operation has no effect + --> tests/ui/identity_op.rs:321:5 + | +LL | 0usize + &<usize as Default>::default(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `<usize as Default>::default()` + +error: this operation has no effect + --> tests/ui/identity_op.rs:324:13 + | +LL | let _ = 0usize + &usize::default(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `usize::default()` + +error: this operation has no effect + --> tests/ui/identity_op.rs:327:21 + | +LL | let _n: usize = 0usize + &Default::default(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `Default::default()` + +error: this operation has no effect + --> tests/ui/identity_op.rs:333:5 + | +LL | 0usize + &Default::default() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `Default::default()` + +error: this operation has no effect + --> tests/ui/identity_op.rs:352:5 + | +LL | 0usize + &<usize as Def>::def(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `<usize as Def>::def()` + +error: this operation has no effect + --> tests/ui/identity_op.rs:355:13 + | +LL | let _ = 0usize + &usize::def(); + | ^^^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `usize::def()` + +error: this operation has no effect + --> tests/ui/identity_op.rs:358:21 + | +LL | let _n: usize = 0usize + &Def::def(); + | ^^^^^^^^^^^^^^^^^^^^ help: consider reducing it to: `Def::def()` + +error: aborting due to 70 previous errors diff --git a/src/tools/clippy/tests/ui/infinite_iter.rs b/src/tools/clippy/tests/ui/infinite_iter.rs index 002a791a657..701a86534ba 100644 --- a/src/tools/clippy/tests/ui/infinite_iter.rs +++ b/src/tools/clippy/tests/ui/infinite_iter.rs @@ -38,7 +38,7 @@ fn infinite_iters() { //~^ infinite_iter // infinite iter - (0_u64..).filter(|x| x % 2 == 0).last(); + (0_u64..).filter(|x| x.is_multiple_of(2)).last(); //~^ infinite_iter // not an infinite, because ranges are double-ended diff --git a/src/tools/clippy/tests/ui/infinite_iter.stderr b/src/tools/clippy/tests/ui/infinite_iter.stderr index 47133a2ea62..b9e7c008f93 100644 --- a/src/tools/clippy/tests/ui/infinite_iter.stderr +++ b/src/tools/clippy/tests/ui/infinite_iter.stderr @@ -42,8 +42,8 @@ LL | (0_usize..).flat_map(|x| 0..x).product::<usize>(); error: infinite iteration detected --> tests/ui/infinite_iter.rs:41:5 | -LL | (0_u64..).filter(|x| x % 2 == 0).last(); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | (0_u64..).filter(|x| x.is_multiple_of(2)).last(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: possible infinite iteration detected --> tests/ui/infinite_iter.rs:53:5 diff --git a/src/tools/clippy/tests/ui/iter_kv_map.fixed b/src/tools/clippy/tests/ui/iter_kv_map.fixed index 874f749b33d..b18dda35887 100644 --- a/src/tools/clippy/tests/ui/iter_kv_map.fixed +++ b/src/tools/clippy/tests/ui/iter_kv_map.fixed @@ -30,15 +30,19 @@ fn main() { let _ = map.clone().values().collect::<Vec<_>>(); //~^ iter_kv_map - let _ = map.keys().filter(|x| *x % 2 == 0).count(); + let _ = map.keys().filter(|x| x.is_multiple_of(2)).count(); //~^ iter_kv_map // Don't lint - let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count(); + let _ = map + .iter() + .filter(|(_, val)| val.is_multiple_of(2)) + .map(|(key, _)| key) + .count(); let _ = map.iter().map(get_key).collect::<Vec<_>>(); // Linting the following could be an improvement to the lint - // map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count(); + // map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count(); // Lint let _ = map.keys().map(|key| key * 9).count(); @@ -84,15 +88,19 @@ fn main() { let _ = map.clone().values().collect::<Vec<_>>(); //~^ iter_kv_map - let _ = map.keys().filter(|x| *x % 2 == 0).count(); + let _ = map.keys().filter(|x| x.is_multiple_of(2)).count(); //~^ iter_kv_map // Don't lint - let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count(); + let _ = map + .iter() + .filter(|(_, val)| val.is_multiple_of(2)) + .map(|(key, _)| key) + .count(); let _ = map.iter().map(get_key).collect::<Vec<_>>(); // Linting the following could be an improvement to the lint - // map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count(); + // map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count(); // Lint let _ = map.keys().map(|key| key * 9).count(); diff --git a/src/tools/clippy/tests/ui/iter_kv_map.rs b/src/tools/clippy/tests/ui/iter_kv_map.rs index f570e3c32cb..729e4e8a266 100644 --- a/src/tools/clippy/tests/ui/iter_kv_map.rs +++ b/src/tools/clippy/tests/ui/iter_kv_map.rs @@ -30,15 +30,19 @@ fn main() { let _ = map.clone().iter().map(|(_, val)| val).collect::<Vec<_>>(); //~^ iter_kv_map - let _ = map.iter().map(|(key, _)| key).filter(|x| *x % 2 == 0).count(); + let _ = map.iter().map(|(key, _)| key).filter(|x| x.is_multiple_of(2)).count(); //~^ iter_kv_map // Don't lint - let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count(); + let _ = map + .iter() + .filter(|(_, val)| val.is_multiple_of(2)) + .map(|(key, _)| key) + .count(); let _ = map.iter().map(get_key).collect::<Vec<_>>(); // Linting the following could be an improvement to the lint - // map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count(); + // map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count(); // Lint let _ = map.iter().map(|(key, _value)| key * 9).count(); @@ -86,15 +90,19 @@ fn main() { let _ = map.clone().iter().map(|(_, val)| val).collect::<Vec<_>>(); //~^ iter_kv_map - let _ = map.iter().map(|(key, _)| key).filter(|x| *x % 2 == 0).count(); + let _ = map.iter().map(|(key, _)| key).filter(|x| x.is_multiple_of(2)).count(); //~^ iter_kv_map // Don't lint - let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count(); + let _ = map + .iter() + .filter(|(_, val)| val.is_multiple_of(2)) + .map(|(key, _)| key) + .count(); let _ = map.iter().map(get_key).collect::<Vec<_>>(); // Linting the following could be an improvement to the lint - // map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count(); + // map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count(); // Lint let _ = map.iter().map(|(key, _value)| key * 9).count(); diff --git a/src/tools/clippy/tests/ui/iter_kv_map.stderr b/src/tools/clippy/tests/ui/iter_kv_map.stderr index 31ee76c25b7..8f73541f503 100644 --- a/src/tools/clippy/tests/ui/iter_kv_map.stderr +++ b/src/tools/clippy/tests/ui/iter_kv_map.stderr @@ -52,29 +52,29 @@ LL | let _ = map.clone().iter().map(|(_, val)| val).collect::<Vec<_>>(); error: iterating on a map's keys --> tests/ui/iter_kv_map.rs:33:13 | -LL | let _ = map.iter().map(|(key, _)| key).filter(|x| *x % 2 == 0).count(); +LL | let _ = map.iter().map(|(key, _)| key).filter(|x| x.is_multiple_of(2)).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.keys()` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:44:13 + --> tests/ui/iter_kv_map.rs:48:13 | LL | let _ = map.iter().map(|(key, _value)| key * 9).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.keys().map(|key| key * 9)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:46:13 + --> tests/ui/iter_kv_map.rs:50:13 | LL | let _ = map.iter().map(|(_key, value)| value * 17).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values().map(|value| value * 17)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:50:13 + --> tests/ui/iter_kv_map.rs:54:13 | LL | let _ = map.clone().into_iter().map(|(_, ref val)| ref_acceptor(val)).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values().map(|ref val| ref_acceptor(val))` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:54:13 + --> tests/ui/iter_kv_map.rs:58:13 | LL | let _ = map | _____________^ @@ -97,85 +97,85 @@ LL + }) | error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:65:13 + --> tests/ui/iter_kv_map.rs:69:13 | LL | let _ = map.clone().into_iter().map(|(_, mut val)| val).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values()` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:70:13 + --> tests/ui/iter_kv_map.rs:74:13 | LL | let _ = map.iter().map(|(key, _)| key).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.keys()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:72:13 + --> tests/ui/iter_kv_map.rs:76:13 | LL | let _ = map.iter().map(|(_, value)| value).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:74:13 + --> tests/ui/iter_kv_map.rs:78:13 | LL | let _ = map.iter().map(|(_, v)| v + 2).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values().map(|v| v + 2)` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:77:13 + --> tests/ui/iter_kv_map.rs:81:13 | LL | let _ = map.clone().into_iter().map(|(key, _)| key).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_keys()` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:79:13 + --> tests/ui/iter_kv_map.rs:83:13 | LL | let _ = map.clone().into_iter().map(|(key, _)| key + 2).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_keys().map(|key| key + 2)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:82:13 + --> tests/ui/iter_kv_map.rs:86:13 | LL | let _ = map.clone().into_iter().map(|(_, val)| val).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:84:13 + --> tests/ui/iter_kv_map.rs:88:13 | LL | let _ = map.clone().into_iter().map(|(_, val)| val + 2).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values().map(|val| val + 2)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:87:13 + --> tests/ui/iter_kv_map.rs:91:13 | LL | let _ = map.clone().iter().map(|(_, val)| val).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().values()` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:89:13 + --> tests/ui/iter_kv_map.rs:93:13 | -LL | let _ = map.iter().map(|(key, _)| key).filter(|x| *x % 2 == 0).count(); +LL | let _ = map.iter().map(|(key, _)| key).filter(|x| x.is_multiple_of(2)).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.keys()` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:100:13 + --> tests/ui/iter_kv_map.rs:108:13 | LL | let _ = map.iter().map(|(key, _value)| key * 9).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.keys().map(|key| key * 9)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:102:13 + --> tests/ui/iter_kv_map.rs:110:13 | LL | let _ = map.iter().map(|(_key, value)| value * 17).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values().map(|value| value * 17)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:106:13 + --> tests/ui/iter_kv_map.rs:114:13 | LL | let _ = map.clone().into_iter().map(|(_, ref val)| ref_acceptor(val)).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values().map(|ref val| ref_acceptor(val))` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:110:13 + --> tests/ui/iter_kv_map.rs:118:13 | LL | let _ = map | _____________^ @@ -198,73 +198,73 @@ LL + }) | error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:121:13 + --> tests/ui/iter_kv_map.rs:129:13 | LL | let _ = map.clone().into_iter().map(|(_, mut val)| val).count(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values()` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:137:13 + --> tests/ui/iter_kv_map.rs:145:13 | LL | let _ = map.iter().map(|(key, _)| key).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.keys()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:140:13 + --> tests/ui/iter_kv_map.rs:148:13 | LL | let _ = map.iter().map(|(_, value)| value).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:143:13 + --> tests/ui/iter_kv_map.rs:151:13 | LL | let _ = map.iter().map(|(_, v)| v + 2).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values().map(|v| v + 2)` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:152:13 + --> tests/ui/iter_kv_map.rs:160:13 | LL | let _ = map.clone().into_iter().map(|(key, _)| key).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_keys()` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:155:13 + --> tests/ui/iter_kv_map.rs:163:13 | LL | let _ = map.clone().into_iter().map(|(key, _)| key + 2).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_keys().map(|key| key + 2)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:158:13 + --> tests/ui/iter_kv_map.rs:166:13 | LL | let _ = map.clone().into_iter().map(|(_, val)| val).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:161:13 + --> tests/ui/iter_kv_map.rs:169:13 | LL | let _ = map.clone().into_iter().map(|(_, val)| val + 2).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.clone().into_values().map(|val| val + 2)` error: iterating on a map's keys - --> tests/ui/iter_kv_map.rs:164:13 + --> tests/ui/iter_kv_map.rs:172:13 | LL | let _ = map.iter().map(|(key, _)| key).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.keys()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:167:13 + --> tests/ui/iter_kv_map.rs:175:13 | LL | let _ = map.iter().map(|(_, value)| value).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values()` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:170:13 + --> tests/ui/iter_kv_map.rs:178:13 | LL | let _ = map.iter().map(|(_, v)| v + 2).collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.values().map(|v| v + 2)` error: iterating on a map's values - --> tests/ui/iter_kv_map.rs:185:13 + --> tests/ui/iter_kv_map.rs:193:13 | LL | let _ = map.as_ref().iter().map(|(_, v)| v).copied().collect::<Vec<_>>(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `map.as_ref().values()` diff --git a/src/tools/clippy/tests/ui/let_unit.fixed b/src/tools/clippy/tests/ui/let_unit.fixed index 5e7a2ad37a8..304eacecd94 100644 --- a/src/tools/clippy/tests/ui/let_unit.fixed +++ b/src/tools/clippy/tests/ui/let_unit.fixed @@ -61,7 +61,7 @@ fn multiline_sugg() { //~^ let_unit_value .into_iter() .map(|i| i * 2) - .filter(|i| i % 2 == 0) + .filter(|i| i.is_multiple_of(2)) .map(|_| ()) .next() .unwrap(); diff --git a/src/tools/clippy/tests/ui/let_unit.rs b/src/tools/clippy/tests/ui/let_unit.rs index 7b06f694012..a02cb346ff9 100644 --- a/src/tools/clippy/tests/ui/let_unit.rs +++ b/src/tools/clippy/tests/ui/let_unit.rs @@ -61,7 +61,7 @@ fn multiline_sugg() { //~^ let_unit_value .into_iter() .map(|i| i * 2) - .filter(|i| i % 2 == 0) + .filter(|i| i.is_multiple_of(2)) .map(|_| ()) .next() .unwrap(); diff --git a/src/tools/clippy/tests/ui/let_unit.stderr b/src/tools/clippy/tests/ui/let_unit.stderr index d7d01d304ca..d743110c99d 100644 --- a/src/tools/clippy/tests/ui/let_unit.stderr +++ b/src/tools/clippy/tests/ui/let_unit.stderr @@ -25,7 +25,7 @@ LL ~ v LL + LL + .into_iter() LL + .map(|i| i * 2) -LL + .filter(|i| i % 2 == 0) +LL + .filter(|i| i.is_multiple_of(2)) LL + .map(|_| ()) LL + .next() LL + .unwrap(); diff --git a/src/tools/clippy/tests/ui/manual_contains.fixed b/src/tools/clippy/tests/ui/manual_contains.fixed index d26c948a781..18171f0b2b4 100644 --- a/src/tools/clippy/tests/ui/manual_contains.fixed +++ b/src/tools/clippy/tests/ui/manual_contains.fixed @@ -58,7 +58,7 @@ fn should_not_lint() { let vec: Vec<u32> = vec![1, 2, 3, 4, 5, 6]; let values = &vec[..]; - let _ = values.iter().any(|&v| v % 2 == 0); + let _ = values.iter().any(|&v| v.is_multiple_of(2)); let _ = values.iter().any(|&v| v * 2 == 6); let _ = values.iter().any(|&v| v == v); let _ = values.iter().any(|&v| 4 == 4); diff --git a/src/tools/clippy/tests/ui/manual_contains.rs b/src/tools/clippy/tests/ui/manual_contains.rs index fe67d2ee5d5..918f4d6b8dd 100644 --- a/src/tools/clippy/tests/ui/manual_contains.rs +++ b/src/tools/clippy/tests/ui/manual_contains.rs @@ -58,7 +58,7 @@ fn should_not_lint() { let vec: Vec<u32> = vec![1, 2, 3, 4, 5, 6]; let values = &vec[..]; - let _ = values.iter().any(|&v| v % 2 == 0); + let _ = values.iter().any(|&v| v.is_multiple_of(2)); let _ = values.iter().any(|&v| v * 2 == 6); let _ = values.iter().any(|&v| v == v); let _ = values.iter().any(|&v| 4 == 4); diff --git a/src/tools/clippy/tests/ui/manual_find_fixable.fixed b/src/tools/clippy/tests/ui/manual_find_fixable.fixed index 01b3ebacbeb..c69b0cb11e3 100644 --- a/src/tools/clippy/tests/ui/manual_find_fixable.fixed +++ b/src/tools/clippy/tests/ui/manual_find_fixable.fixed @@ -11,7 +11,7 @@ fn lookup(n: u32) -> Option<u32> { } fn with_pat(arr: Vec<(u32, u32)>) -> Option<u32> { - arr.into_iter().map(|(a, _)| a).find(|&a| a % 2 == 0) + arr.into_iter().map(|(a, _)| a).find(|&a| a.is_multiple_of(2)) } struct Data { @@ -63,7 +63,7 @@ fn with_side_effects(arr: Vec<u32>) -> Option<u32> { fn with_else(arr: Vec<u32>) -> Option<u32> { for el in arr { - if el % 2 == 0 { + if el.is_multiple_of(2) { return Some(el); } else { println!("{}", el); diff --git a/src/tools/clippy/tests/ui/manual_find_fixable.rs b/src/tools/clippy/tests/ui/manual_find_fixable.rs index ce62a4beba1..db7092f020c 100644 --- a/src/tools/clippy/tests/ui/manual_find_fixable.rs +++ b/src/tools/clippy/tests/ui/manual_find_fixable.rs @@ -19,7 +19,7 @@ fn lookup(n: u32) -> Option<u32> { fn with_pat(arr: Vec<(u32, u32)>) -> Option<u32> { for (a, _) in arr { //~^ manual_find - if a % 2 == 0 { + if a.is_multiple_of(2) { return Some(a); } } @@ -111,7 +111,7 @@ fn with_side_effects(arr: Vec<u32>) -> Option<u32> { fn with_else(arr: Vec<u32>) -> Option<u32> { for el in arr { - if el % 2 == 0 { + if el.is_multiple_of(2) { return Some(el); } else { println!("{}", el); diff --git a/src/tools/clippy/tests/ui/manual_find_fixable.stderr b/src/tools/clippy/tests/ui/manual_find_fixable.stderr index 020635d90bb..0c05c0d2c44 100644 --- a/src/tools/clippy/tests/ui/manual_find_fixable.stderr +++ b/src/tools/clippy/tests/ui/manual_find_fixable.stderr @@ -17,11 +17,11 @@ error: manual implementation of `Iterator::find` | LL | / for (a, _) in arr { LL | | -LL | | if a % 2 == 0 { +LL | | if a.is_multiple_of(2) { LL | | return Some(a); ... | LL | | None - | |________^ help: replace with an iterator: `arr.into_iter().map(|(a, _)| a).find(|&a| a % 2 == 0)` + | |________^ help: replace with an iterator: `arr.into_iter().map(|(a, _)| a).find(|&a| a.is_multiple_of(2))` error: manual implementation of `Iterator::find` --> tests/ui/manual_find_fixable.rs:34:5 diff --git a/src/tools/clippy/tests/ui/manual_is_multiple_of.fixed b/src/tools/clippy/tests/ui/manual_is_multiple_of.fixed new file mode 100644 index 00000000000..6735b99f298 --- /dev/null +++ b/src/tools/clippy/tests/ui/manual_is_multiple_of.fixed @@ -0,0 +1,25 @@ +//@aux-build: proc_macros.rs +#![warn(clippy::manual_is_multiple_of)] + +fn main() {} + +#[clippy::msrv = "1.87"] +fn f(a: u64, b: u64) { + let _ = a.is_multiple_of(b); //~ manual_is_multiple_of + let _ = (a + 1).is_multiple_of(b + 1); //~ manual_is_multiple_of + let _ = !a.is_multiple_of(b); //~ manual_is_multiple_of + let _ = !(a + 1).is_multiple_of(b + 1); //~ manual_is_multiple_of + + let _ = !a.is_multiple_of(b); //~ manual_is_multiple_of + let _ = !a.is_multiple_of(b); //~ manual_is_multiple_of + + proc_macros::external! { + let a: u64 = 23424; + let _ = a % 4096 == 0; + } +} + +#[clippy::msrv = "1.86"] +fn g(a: u64, b: u64) { + let _ = a % b == 0; +} diff --git a/src/tools/clippy/tests/ui/manual_is_multiple_of.rs b/src/tools/clippy/tests/ui/manual_is_multiple_of.rs new file mode 100644 index 00000000000..00b638e4fd9 --- /dev/null +++ b/src/tools/clippy/tests/ui/manual_is_multiple_of.rs @@ -0,0 +1,25 @@ +//@aux-build: proc_macros.rs +#![warn(clippy::manual_is_multiple_of)] + +fn main() {} + +#[clippy::msrv = "1.87"] +fn f(a: u64, b: u64) { + let _ = a % b == 0; //~ manual_is_multiple_of + let _ = (a + 1) % (b + 1) == 0; //~ manual_is_multiple_of + let _ = a % b != 0; //~ manual_is_multiple_of + let _ = (a + 1) % (b + 1) != 0; //~ manual_is_multiple_of + + let _ = a % b > 0; //~ manual_is_multiple_of + let _ = 0 < a % b; //~ manual_is_multiple_of + + proc_macros::external! { + let a: u64 = 23424; + let _ = a % 4096 == 0; + } +} + +#[clippy::msrv = "1.86"] +fn g(a: u64, b: u64) { + let _ = a % b == 0; +} diff --git a/src/tools/clippy/tests/ui/manual_is_multiple_of.stderr b/src/tools/clippy/tests/ui/manual_is_multiple_of.stderr new file mode 100644 index 00000000000..0b1ae70c2a7 --- /dev/null +++ b/src/tools/clippy/tests/ui/manual_is_multiple_of.stderr @@ -0,0 +1,41 @@ +error: manual implementation of `.is_multiple_of()` + --> tests/ui/manual_is_multiple_of.rs:8:13 + | +LL | let _ = a % b == 0; + | ^^^^^^^^^^ help: replace with: `a.is_multiple_of(b)` + | + = note: `-D clippy::manual-is-multiple-of` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::manual_is_multiple_of)]` + +error: manual implementation of `.is_multiple_of()` + --> tests/ui/manual_is_multiple_of.rs:9:13 + | +LL | let _ = (a + 1) % (b + 1) == 0; + | ^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `(a + 1).is_multiple_of(b + 1)` + +error: manual implementation of `.is_multiple_of()` + --> tests/ui/manual_is_multiple_of.rs:10:13 + | +LL | let _ = a % b != 0; + | ^^^^^^^^^^ help: replace with: `!a.is_multiple_of(b)` + +error: manual implementation of `.is_multiple_of()` + --> tests/ui/manual_is_multiple_of.rs:11:13 + | +LL | let _ = (a + 1) % (b + 1) != 0; + | ^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `!(a + 1).is_multiple_of(b + 1)` + +error: manual implementation of `.is_multiple_of()` + --> tests/ui/manual_is_multiple_of.rs:13:13 + | +LL | let _ = a % b > 0; + | ^^^^^^^^^ help: replace with: `!a.is_multiple_of(b)` + +error: manual implementation of `.is_multiple_of()` + --> tests/ui/manual_is_multiple_of.rs:14:13 + | +LL | let _ = 0 < a % b; + | ^^^^^^^^^ help: replace with: `!a.is_multiple_of(b)` + +error: aborting due to 6 previous errors + diff --git a/src/tools/clippy/tests/ui/manual_is_variant_and.fixed b/src/tools/clippy/tests/ui/manual_is_variant_and.fixed index 18a72188ab5..6425f32c09c 100644 --- a/src/tools/clippy/tests/ui/manual_is_variant_and.fixed +++ b/src/tools/clippy/tests/ui/manual_is_variant_and.fixed @@ -77,7 +77,7 @@ fn option_methods() { let _ = opt_map!(opt2, |x| x == 'a').unwrap_or_default(); // should not lint // Should not lint. - let _ = Foo::<u32>(0).map(|x| x % 2 == 0) == Some(true); + let _ = Foo::<u32>(0).map(|x| x.is_multiple_of(2)) == Some(true); let _ = Some(2).map(|x| x % 2 == 0) != foo(); let _ = mac!(eq Some(2).map(|x| x % 2 == 0), Some(true)); let _ = mac!(some 2).map(|x| x % 2 == 0) == Some(true); @@ -96,11 +96,11 @@ fn result_methods() { }); let _ = res.is_ok_and(|x| x > 1); - let _ = Ok::<usize, ()>(2).is_ok_and(|x| x % 2 == 0); + let _ = Ok::<usize, ()>(2).is_ok_and(|x| x.is_multiple_of(2)); //~^ manual_is_variant_and - let _ = !Ok::<usize, ()>(2).is_ok_and(|x| x % 2 == 0); + let _ = !Ok::<usize, ()>(2).is_ok_and(|x| x.is_multiple_of(2)); //~^ manual_is_variant_and - let _ = !Ok::<usize, ()>(2).is_ok_and(|x| x % 2 == 0); + let _ = !Ok::<usize, ()>(2).is_ok_and(|x| x.is_multiple_of(2)); //~^ manual_is_variant_and // won't fix because the return type of the closure is not `bool` diff --git a/src/tools/clippy/tests/ui/manual_is_variant_and.rs b/src/tools/clippy/tests/ui/manual_is_variant_and.rs index a92f7c04369..e069e97a04d 100644 --- a/src/tools/clippy/tests/ui/manual_is_variant_and.rs +++ b/src/tools/clippy/tests/ui/manual_is_variant_and.rs @@ -83,7 +83,7 @@ fn option_methods() { let _ = opt_map!(opt2, |x| x == 'a').unwrap_or_default(); // should not lint // Should not lint. - let _ = Foo::<u32>(0).map(|x| x % 2 == 0) == Some(true); + let _ = Foo::<u32>(0).map(|x| x.is_multiple_of(2)) == Some(true); let _ = Some(2).map(|x| x % 2 == 0) != foo(); let _ = mac!(eq Some(2).map(|x| x % 2 == 0), Some(true)); let _ = mac!(some 2).map(|x| x % 2 == 0) == Some(true); @@ -105,11 +105,11 @@ fn result_methods() { //~^ manual_is_variant_and .unwrap_or_default(); - let _ = Ok::<usize, ()>(2).map(|x| x % 2 == 0) == Ok(true); + let _ = Ok::<usize, ()>(2).map(|x| x.is_multiple_of(2)) == Ok(true); //~^ manual_is_variant_and - let _ = Ok::<usize, ()>(2).map(|x| x % 2 == 0) != Ok(true); + let _ = Ok::<usize, ()>(2).map(|x| x.is_multiple_of(2)) != Ok(true); //~^ manual_is_variant_and - let _ = Ok::<usize, ()>(2).map(|x| x % 2 == 0) != Ok(true); + let _ = Ok::<usize, ()>(2).map(|x| x.is_multiple_of(2)) != Ok(true); //~^ manual_is_variant_and // won't fix because the return type of the closure is not `bool` diff --git a/src/tools/clippy/tests/ui/manual_is_variant_and.stderr b/src/tools/clippy/tests/ui/manual_is_variant_and.stderr index 1fb437a8bc7..f770319a268 100644 --- a/src/tools/clippy/tests/ui/manual_is_variant_and.stderr +++ b/src/tools/clippy/tests/ui/manual_is_variant_and.stderr @@ -105,20 +105,20 @@ LL | | .unwrap_or_default(); error: called `.map() == Ok()` --> tests/ui/manual_is_variant_and.rs:108:13 | -LL | let _ = Ok::<usize, ()>(2).map(|x| x % 2 == 0) == Ok(true); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ok::<usize, ()>(2).is_ok_and(|x| x % 2 == 0)` +LL | let _ = Ok::<usize, ()>(2).map(|x| x.is_multiple_of(2)) == Ok(true); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ok::<usize, ()>(2).is_ok_and(|x| x.is_multiple_of(2))` error: called `.map() != Ok()` --> tests/ui/manual_is_variant_and.rs:110:13 | -LL | let _ = Ok::<usize, ()>(2).map(|x| x % 2 == 0) != Ok(true); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `!Ok::<usize, ()>(2).is_ok_and(|x| x % 2 == 0)` +LL | let _ = Ok::<usize, ()>(2).map(|x| x.is_multiple_of(2)) != Ok(true); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `!Ok::<usize, ()>(2).is_ok_and(|x| x.is_multiple_of(2))` error: called `.map() != Ok()` --> tests/ui/manual_is_variant_and.rs:112:13 | -LL | let _ = Ok::<usize, ()>(2).map(|x| x % 2 == 0) != Ok(true); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `!Ok::<usize, ()>(2).is_ok_and(|x| x % 2 == 0)` +LL | let _ = Ok::<usize, ()>(2).map(|x| x.is_multiple_of(2)) != Ok(true); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `!Ok::<usize, ()>(2).is_ok_and(|x| x.is_multiple_of(2))` error: called `map(<f>).unwrap_or_default()` on a `Result` value --> tests/ui/manual_is_variant_and.rs:119:18 diff --git a/src/tools/clippy/tests/ui/manual_ok_err.fixed b/src/tools/clippy/tests/ui/manual_ok_err.fixed index e6f799aa58d..9b70ce0df43 100644 --- a/src/tools/clippy/tests/ui/manual_ok_err.fixed +++ b/src/tools/clippy/tests/ui/manual_ok_err.fixed @@ -103,3 +103,27 @@ fn issue14239() { }; //~^^^^^ manual_ok_err } + +mod issue15051 { + struct Container { + field: Result<bool, ()>, + } + + #[allow(clippy::needless_borrow)] + fn with_addr_of(x: &Container) -> Option<&bool> { + (&x.field).as_ref().ok() + } + + fn from_fn(x: &Container) -> Option<&bool> { + let result_with_ref = || &x.field; + result_with_ref().as_ref().ok() + } + + fn result_with_ref_mut(x: &mut Container) -> &mut Result<bool, ()> { + &mut x.field + } + + fn from_fn_mut(x: &mut Container) -> Option<&mut bool> { + result_with_ref_mut(x).as_mut().ok() + } +} diff --git a/src/tools/clippy/tests/ui/manual_ok_err.rs b/src/tools/clippy/tests/ui/manual_ok_err.rs index 972b2c41ee7..dee90463824 100644 --- a/src/tools/clippy/tests/ui/manual_ok_err.rs +++ b/src/tools/clippy/tests/ui/manual_ok_err.rs @@ -141,3 +141,39 @@ fn issue14239() { }; //~^^^^^ manual_ok_err } + +mod issue15051 { + struct Container { + field: Result<bool, ()>, + } + + #[allow(clippy::needless_borrow)] + fn with_addr_of(x: &Container) -> Option<&bool> { + match &x.field { + //~^ manual_ok_err + Ok(panel) => Some(panel), + Err(_) => None, + } + } + + fn from_fn(x: &Container) -> Option<&bool> { + let result_with_ref = || &x.field; + match result_with_ref() { + //~^ manual_ok_err + Ok(panel) => Some(panel), + Err(_) => None, + } + } + + fn result_with_ref_mut(x: &mut Container) -> &mut Result<bool, ()> { + &mut x.field + } + + fn from_fn_mut(x: &mut Container) -> Option<&mut bool> { + match result_with_ref_mut(x) { + //~^ manual_ok_err + Ok(panel) => Some(panel), + Err(_) => None, + } + } +} diff --git a/src/tools/clippy/tests/ui/manual_ok_err.stderr b/src/tools/clippy/tests/ui/manual_ok_err.stderr index 040e170f397..448fbffc050 100644 --- a/src/tools/clippy/tests/ui/manual_ok_err.stderr +++ b/src/tools/clippy/tests/ui/manual_ok_err.stderr @@ -111,5 +111,35 @@ LL + "1".parse::<u8>().ok() LL ~ }; | -error: aborting due to 9 previous errors +error: manual implementation of `ok` + --> tests/ui/manual_ok_err.rs:152:9 + | +LL | / match &x.field { +LL | | +LL | | Ok(panel) => Some(panel), +LL | | Err(_) => None, +LL | | } + | |_________^ help: replace with: `(&x.field).as_ref().ok()` + +error: manual implementation of `ok` + --> tests/ui/manual_ok_err.rs:161:9 + | +LL | / match result_with_ref() { +LL | | +LL | | Ok(panel) => Some(panel), +LL | | Err(_) => None, +LL | | } + | |_________^ help: replace with: `result_with_ref().as_ref().ok()` + +error: manual implementation of `ok` + --> tests/ui/manual_ok_err.rs:173:9 + | +LL | / match result_with_ref_mut(x) { +LL | | +LL | | Ok(panel) => Some(panel), +LL | | Err(_) => None, +LL | | } + | |_________^ help: replace with: `result_with_ref_mut(x).as_mut().ok()` + +error: aborting due to 12 previous errors diff --git a/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.fixed b/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.fixed index 7e0d4fccaae..f1d5579a723 100644 --- a/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.fixed +++ b/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.fixed @@ -25,7 +25,7 @@ const fn can_be_const() { 0u64.method(); } -// False negative, see FIXME comment in `clipy_utils::qualify_min_const` +// False negative, see FIXME comment in `clippy_utils::qualify_min_const_fn` fn could_be_const_but_does_not_trigger<T>(t: T) where T: const ConstTrait, diff --git a/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.rs b/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.rs index 439da4622d7..d495759526d 100644 --- a/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.rs +++ b/src/tools/clippy/tests/ui/missing_const_for_fn/const_trait.rs @@ -25,7 +25,7 @@ fn can_be_const() { 0u64.method(); } -// False negative, see FIXME comment in `clipy_utils::qualify_min_const` +// False negative, see FIXME comment in `clippy_utils::qualify_min_const_fn` fn could_be_const_but_does_not_trigger<T>(t: T) where T: const ConstTrait, diff --git a/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.fixed b/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.fixed index 65eb2d5938b..95bf63ed1df 100644 --- a/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.fixed +++ b/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.fixed @@ -221,3 +221,60 @@ const fn mut_add(x: &mut i32) { //~^ missing_const_for_fn *x += 1; } + +mod issue_15079 { + pub trait Trait {} + + pub struct Struct<T: Trait> { + _t: Option<T>, + } + + impl<T: Trait> Struct<T> { + #[clippy::msrv = "1.60"] + pub fn new_1_60() -> Self { + Self { _t: None } + } + + #[clippy::msrv = "1.61"] + pub const fn new_1_61() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + } + + pub struct S2<T> { + _t: Option<T>, + } + + impl<T> S2<T> { + #[clippy::msrv = "1.60"] + pub const fn new_1_60() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + + #[clippy::msrv = "1.61"] + pub const fn new_1_61() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + } + + pub struct S3<T: ?Sized + 'static> { + _t: Option<&'static T>, + } + + impl<T: ?Sized + 'static> S3<T> { + #[clippy::msrv = "1.60"] + pub const fn new_1_60() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + + #[clippy::msrv = "1.61"] + pub const fn new_1_61() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + } +} diff --git a/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.rs b/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.rs index 3690d2f799f..8290be67546 100644 --- a/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.rs +++ b/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.rs @@ -221,3 +221,60 @@ fn mut_add(x: &mut i32) { //~^ missing_const_for_fn *x += 1; } + +mod issue_15079 { + pub trait Trait {} + + pub struct Struct<T: Trait> { + _t: Option<T>, + } + + impl<T: Trait> Struct<T> { + #[clippy::msrv = "1.60"] + pub fn new_1_60() -> Self { + Self { _t: None } + } + + #[clippy::msrv = "1.61"] + pub fn new_1_61() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + } + + pub struct S2<T> { + _t: Option<T>, + } + + impl<T> S2<T> { + #[clippy::msrv = "1.60"] + pub fn new_1_60() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + + #[clippy::msrv = "1.61"] + pub fn new_1_61() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + } + + pub struct S3<T: ?Sized + 'static> { + _t: Option<&'static T>, + } + + impl<T: ?Sized + 'static> S3<T> { + #[clippy::msrv = "1.60"] + pub fn new_1_60() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + + #[clippy::msrv = "1.61"] + pub fn new_1_61() -> Self { + //~^ missing_const_for_fn + Self { _t: None } + } + } +} diff --git a/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.stderr b/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.stderr index 10e07d12f5a..17cbc431276 100644 --- a/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.stderr +++ b/src/tools/clippy/tests/ui/missing_const_for_fn/could_be_const.stderr @@ -332,5 +332,75 @@ help: make the function `const` LL | const fn mut_add(x: &mut i32) { | +++++ -error: aborting due to 25 previous errors +error: this could be a `const fn` + --> tests/ui/missing_const_for_fn/could_be_const.rs:239:9 + | +LL | / pub fn new_1_61() -> Self { +LL | | +LL | | Self { _t: None } +LL | | } + | |_________^ + | +help: make the function `const` + | +LL | pub const fn new_1_61() -> Self { + | +++++ + +error: this could be a `const fn` + --> tests/ui/missing_const_for_fn/could_be_const.rs:251:9 + | +LL | / pub fn new_1_60() -> Self { +LL | | +LL | | Self { _t: None } +LL | | } + | |_________^ + | +help: make the function `const` + | +LL | pub const fn new_1_60() -> Self { + | +++++ + +error: this could be a `const fn` + --> tests/ui/missing_const_for_fn/could_be_const.rs:257:9 + | +LL | / pub fn new_1_61() -> Self { +LL | | +LL | | Self { _t: None } +LL | | } + | |_________^ + | +help: make the function `const` + | +LL | pub const fn new_1_61() -> Self { + | +++++ + +error: this could be a `const fn` + --> tests/ui/missing_const_for_fn/could_be_const.rs:269:9 + | +LL | / pub fn new_1_60() -> Self { +LL | | +LL | | Self { _t: None } +LL | | } + | |_________^ + | +help: make the function `const` + | +LL | pub const fn new_1_60() -> Self { + | +++++ + +error: this could be a `const fn` + --> tests/ui/missing_const_for_fn/could_be_const.rs:275:9 + | +LL | / pub fn new_1_61() -> Self { +LL | | +LL | | Self { _t: None } +LL | | } + | |_________^ + | +help: make the function `const` + | +LL | pub const fn new_1_61() -> Self { + | +++++ + +error: aborting due to 30 previous errors diff --git a/src/tools/clippy/tests/ui/nonminimal_bool.stderr b/src/tools/clippy/tests/ui/nonminimal_bool.stderr index 0e3e4cf7988..ecb82a23da0 100644 --- a/src/tools/clippy/tests/ui/nonminimal_bool.stderr +++ b/src/tools/clippy/tests/ui/nonminimal_bool.stderr @@ -179,7 +179,7 @@ error: inequality checks against true can be replaced by a negation --> tests/ui/nonminimal_bool.rs:186:8 | LL | if !b != true {} - | ^^^^^^^^^^ help: try simplifying it as shown: `!(!b)` + | ^^^^^^^^^^ help: try simplifying it as shown: `!!b` error: this boolean expression can be simplified --> tests/ui/nonminimal_bool.rs:189:8 @@ -209,7 +209,7 @@ error: inequality checks against true can be replaced by a negation --> tests/ui/nonminimal_bool.rs:193:8 | LL | if true != !b {} - | ^^^^^^^^^^ help: try simplifying it as shown: `!(!b)` + | ^^^^^^^^^^ help: try simplifying it as shown: `!!b` error: this boolean expression can be simplified --> tests/ui/nonminimal_bool.rs:196:8 diff --git a/src/tools/clippy/tests/ui/or_fun_call.fixed b/src/tools/clippy/tests/ui/or_fun_call.fixed index a1119d75c23..34f3e046841 100644 --- a/src/tools/clippy/tests/ui/or_fun_call.fixed +++ b/src/tools/clippy/tests/ui/or_fun_call.fixed @@ -5,6 +5,7 @@ clippy::uninlined_format_args, clippy::unnecessary_wraps, clippy::unnecessary_literal_unwrap, + clippy::unnecessary_result_map_or_else, clippy::useless_vec )] @@ -409,4 +410,33 @@ fn fn_call_in_nested_expr() { //~^ or_fun_call } +mod result_map_or { + fn g() -> i32 { + 3 + } + + fn f(n: i32) -> i32 { + n + } + + fn test_map_or() { + let x: Result<i32, ()> = Ok(4); + let _ = x.map_or_else(|_| g(), |v| v); + //~^ or_fun_call + let _ = x.map_or_else(|_| g(), f); + //~^ or_fun_call + let _ = x.map_or(0, f); + } +} + +fn test_option_get_or_insert() { + // assume that this is slow call + fn g() -> u8 { + 99 + } + let mut x = Some(42_u8); + let _ = x.get_or_insert_with(g); + //~^ or_fun_call +} + fn main() {} diff --git a/src/tools/clippy/tests/ui/or_fun_call.rs b/src/tools/clippy/tests/ui/or_fun_call.rs index a7cd632bf16..dc57bd6060a 100644 --- a/src/tools/clippy/tests/ui/or_fun_call.rs +++ b/src/tools/clippy/tests/ui/or_fun_call.rs @@ -5,6 +5,7 @@ clippy::uninlined_format_args, clippy::unnecessary_wraps, clippy::unnecessary_literal_unwrap, + clippy::unnecessary_result_map_or_else, clippy::useless_vec )] @@ -409,4 +410,33 @@ fn fn_call_in_nested_expr() { //~^ or_fun_call } +mod result_map_or { + fn g() -> i32 { + 3 + } + + fn f(n: i32) -> i32 { + n + } + + fn test_map_or() { + let x: Result<i32, ()> = Ok(4); + let _ = x.map_or(g(), |v| v); + //~^ or_fun_call + let _ = x.map_or(g(), f); + //~^ or_fun_call + let _ = x.map_or(0, f); + } +} + +fn test_option_get_or_insert() { + // assume that this is slow call + fn g() -> u8 { + 99 + } + let mut x = Some(42_u8); + let _ = x.get_or_insert(g()); + //~^ or_fun_call +} + fn main() {} diff --git a/src/tools/clippy/tests/ui/or_fun_call.stderr b/src/tools/clippy/tests/ui/or_fun_call.stderr index 35bda7e4d33..0f159fe8bff 100644 --- a/src/tools/clippy/tests/ui/or_fun_call.stderr +++ b/src/tools/clippy/tests/ui/or_fun_call.stderr @@ -1,5 +1,5 @@ error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:52:22 + --> tests/ui/or_fun_call.rs:53:22 | LL | with_constructor.unwrap_or(make()); | ^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(make)` @@ -8,7 +8,7 @@ LL | with_constructor.unwrap_or(make()); = help: to override `-D warnings` add `#[allow(clippy::or_fun_call)]` error: use of `unwrap_or` to construct default value - --> tests/ui/or_fun_call.rs:56:14 + --> tests/ui/or_fun_call.rs:57:14 | LL | with_new.unwrap_or(Vec::new()); | ^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` @@ -17,199 +17,199 @@ LL | with_new.unwrap_or(Vec::new()); = help: to override `-D warnings` add `#[allow(clippy::unwrap_or_default)]` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:60:21 + --> tests/ui/or_fun_call.rs:61:21 | LL | with_const_args.unwrap_or(Vec::with_capacity(12)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| Vec::with_capacity(12))` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:64:14 + --> tests/ui/or_fun_call.rs:65:14 | LL | with_err.unwrap_or(make()); | ^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|_| make())` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:68:19 + --> tests/ui/or_fun_call.rs:69:19 | LL | with_err_args.unwrap_or(Vec::with_capacity(12)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|_| Vec::with_capacity(12))` error: use of `unwrap_or` to construct default value - --> tests/ui/or_fun_call.rs:72:24 + --> tests/ui/or_fun_call.rs:73:24 | LL | with_default_trait.unwrap_or(Default::default()); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: use of `unwrap_or` to construct default value - --> tests/ui/or_fun_call.rs:76:23 + --> tests/ui/or_fun_call.rs:77:23 | LL | with_default_type.unwrap_or(u64::default()); | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:80:18 + --> tests/ui/or_fun_call.rs:81:18 | LL | self_default.unwrap_or(<FakeDefault>::default()); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(<FakeDefault>::default)` error: use of `unwrap_or` to construct default value - --> tests/ui/or_fun_call.rs:84:18 + --> tests/ui/or_fun_call.rs:85:18 | LL | real_default.unwrap_or(<FakeDefault as Default>::default()); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: use of `unwrap_or` to construct default value - --> tests/ui/or_fun_call.rs:88:14 + --> tests/ui/or_fun_call.rs:89:14 | LL | with_vec.unwrap_or(vec![]); | ^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:92:21 + --> tests/ui/or_fun_call.rs:93:21 | LL | without_default.unwrap_or(Foo::new()); | ^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(Foo::new)` error: use of `or_insert` to construct default value - --> tests/ui/or_fun_call.rs:96:19 + --> tests/ui/or_fun_call.rs:97:19 | LL | map.entry(42).or_insert(String::new()); | ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `or_default()` error: use of `or_insert` to construct default value - --> tests/ui/or_fun_call.rs:100:23 + --> tests/ui/or_fun_call.rs:101:23 | LL | map_vec.entry(42).or_insert(vec![]); | ^^^^^^^^^^^^^^^^^ help: try: `or_default()` error: use of `or_insert` to construct default value - --> tests/ui/or_fun_call.rs:104:21 + --> tests/ui/or_fun_call.rs:105:21 | LL | btree.entry(42).or_insert(String::new()); | ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `or_default()` error: use of `or_insert` to construct default value - --> tests/ui/or_fun_call.rs:108:25 + --> tests/ui/or_fun_call.rs:109:25 | LL | btree_vec.entry(42).or_insert(vec![]); | ^^^^^^^^^^^^^^^^^ help: try: `or_default()` error: use of `unwrap_or` to construct default value - --> tests/ui/or_fun_call.rs:112:21 + --> tests/ui/or_fun_call.rs:113:21 | LL | let _ = stringy.unwrap_or(String::new()); | ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: function call inside of `ok_or` - --> tests/ui/or_fun_call.rs:117:17 + --> tests/ui/or_fun_call.rs:118:17 | LL | let _ = opt.ok_or(format!("{} world.", hello)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `ok_or_else(|| format!("{} world.", hello))` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:122:21 + --> tests/ui/or_fun_call.rs:123:21 | LL | let _ = Some(1).unwrap_or(map[&1]); | ^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| map[&1])` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:125:21 + --> tests/ui/or_fun_call.rs:126:21 | LL | let _ = Some(1).unwrap_or(map[&1]); | ^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| map[&1])` error: function call inside of `or` - --> tests/ui/or_fun_call.rs:150:35 + --> tests/ui/or_fun_call.rs:151:35 | LL | let _ = Some("a".to_string()).or(Some("b".to_string())); | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `or_else(|| Some("b".to_string()))` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:193:18 + --> tests/ui/or_fun_call.rs:194:18 | LL | None.unwrap_or(ptr_to_ref(s)); | ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| ptr_to_ref(s))` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:201:14 + --> tests/ui/or_fun_call.rs:202:14 | LL | None.unwrap_or(unsafe { ptr_to_ref(s) }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| unsafe { ptr_to_ref(s) })` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:204:14 + --> tests/ui/or_fun_call.rs:205:14 | LL | None.unwrap_or( unsafe { ptr_to_ref(s) } ); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| unsafe { ptr_to_ref(s) })` error: function call inside of `map_or` - --> tests/ui/or_fun_call.rs:280:25 + --> tests/ui/or_fun_call.rs:281:25 | LL | let _ = Some(4).map_or(g(), |v| v); | ^^^^^^^^^^^^^^^^^^ help: try: `map_or_else(g, |v| v)` error: function call inside of `map_or` - --> tests/ui/or_fun_call.rs:282:25 + --> tests/ui/or_fun_call.rs:283:25 | LL | let _ = Some(4).map_or(g(), f); | ^^^^^^^^^^^^^^ help: try: `map_or_else(g, f)` error: use of `unwrap_or_else` to construct default value - --> tests/ui/or_fun_call.rs:314:18 + --> tests/ui/or_fun_call.rs:315:18 | LL | with_new.unwrap_or_else(Vec::new); | ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: use of `unwrap_or_else` to construct default value - --> tests/ui/or_fun_call.rs:318:28 + --> tests/ui/or_fun_call.rs:319:28 | LL | with_default_trait.unwrap_or_else(Default::default); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: use of `unwrap_or_else` to construct default value - --> tests/ui/or_fun_call.rs:322:27 + --> tests/ui/or_fun_call.rs:323:27 | LL | with_default_type.unwrap_or_else(u64::default); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: use of `unwrap_or_else` to construct default value - --> tests/ui/or_fun_call.rs:326:22 + --> tests/ui/or_fun_call.rs:327:22 | LL | real_default.unwrap_or_else(<FakeDefault as Default>::default); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: use of `or_insert_with` to construct default value - --> tests/ui/or_fun_call.rs:330:23 + --> tests/ui/or_fun_call.rs:331:23 | LL | map.entry(42).or_insert_with(String::new); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `or_default()` error: use of `or_insert_with` to construct default value - --> tests/ui/or_fun_call.rs:334:25 + --> tests/ui/or_fun_call.rs:335:25 | LL | btree.entry(42).or_insert_with(String::new); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `or_default()` error: use of `unwrap_or_else` to construct default value - --> tests/ui/or_fun_call.rs:338:25 + --> tests/ui/or_fun_call.rs:339:25 | LL | let _ = stringy.unwrap_or_else(String::new); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:380:17 + --> tests/ui/or_fun_call.rs:381:17 | LL | let _ = opt.unwrap_or({ f() }); // suggest `.unwrap_or_else(f)` | ^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(f)` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:385:17 + --> tests/ui/or_fun_call.rs:386:17 | LL | let _ = opt.unwrap_or(f() + 1); // suggest `.unwrap_or_else(|| f() + 1)` | ^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| f() + 1)` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:390:17 + --> tests/ui/or_fun_call.rs:391:17 | LL | let _ = opt.unwrap_or({ | _________________^ @@ -229,22 +229,40 @@ LL ~ }); | error: function call inside of `map_or` - --> tests/ui/or_fun_call.rs:396:17 + --> tests/ui/or_fun_call.rs:397:17 | LL | let _ = opt.map_or(f() + 1, |v| v); // suggest `.map_or_else(|| f() + 1, |v| v)` | ^^^^^^^^^^^^^^^^^^^^^^ help: try: `map_or_else(|| f() + 1, |v| v)` error: use of `unwrap_or` to construct default value - --> tests/ui/or_fun_call.rs:401:17 + --> tests/ui/or_fun_call.rs:402:17 | LL | let _ = opt.unwrap_or({ i32::default() }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_default()` error: function call inside of `unwrap_or` - --> tests/ui/or_fun_call.rs:408:21 + --> tests/ui/or_fun_call.rs:409:21 | LL | let _ = opt_foo.unwrap_or(Foo { val: String::default() }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| Foo { val: String::default() })` -error: aborting due to 38 previous errors +error: function call inside of `map_or` + --> tests/ui/or_fun_call.rs:424:19 + | +LL | let _ = x.map_or(g(), |v| v); + | ^^^^^^^^^^^^^^^^^^ help: try: `map_or_else(|_| g(), |v| v)` + +error: function call inside of `map_or` + --> tests/ui/or_fun_call.rs:426:19 + | +LL | let _ = x.map_or(g(), f); + | ^^^^^^^^^^^^^^ help: try: `map_or_else(|_| g(), f)` + +error: function call inside of `get_or_insert` + --> tests/ui/or_fun_call.rs:438:15 + | +LL | let _ = x.get_or_insert(g()); + | ^^^^^^^^^^^^^^^^^^ help: try: `get_or_insert_with(g)` + +error: aborting due to 41 previous errors diff --git a/src/tools/clippy/tests/ui/question_mark.fixed b/src/tools/clippy/tests/ui/question_mark.fixed index 60dc1c101b6..8d6f5fbadca 100644 --- a/src/tools/clippy/tests/ui/question_mark.fixed +++ b/src/tools/clippy/tests/ui/question_mark.fixed @@ -453,3 +453,15 @@ fn const_in_pattern(x: Option<(i32, i32)>) -> Option<()> { None } + +fn issue_13642(x: Option<i32>) -> Option<()> { + let Some(x) = x else { + #[cfg(false)] + panic!(); + + #[cfg(true)] + return None; + }; + + None +} diff --git a/src/tools/clippy/tests/ui/question_mark.rs b/src/tools/clippy/tests/ui/question_mark.rs index 99d0122a98f..f13eee29c11 100644 --- a/src/tools/clippy/tests/ui/question_mark.rs +++ b/src/tools/clippy/tests/ui/question_mark.rs @@ -549,3 +549,15 @@ fn const_in_pattern(x: Option<(i32, i32)>) -> Option<()> { None } + +fn issue_13642(x: Option<i32>) -> Option<()> { + let Some(x) = x else { + #[cfg(false)] + panic!(); + + #[cfg(true)] + return None; + }; + + None +} diff --git a/src/tools/clippy/tests/ui/trait_duplication_in_bounds.fixed b/src/tools/clippy/tests/ui/trait_duplication_in_bounds.fixed index 666ff78b218..cf52ecf2f03 100644 --- a/src/tools/clippy/tests/ui/trait_duplication_in_bounds.fixed +++ b/src/tools/clippy/tests/ui/trait_duplication_in_bounds.fixed @@ -169,9 +169,9 @@ where // #13476 #[const_trait] trait ConstTrait {} -const fn const_trait_bounds_good<T: ConstTrait + ~const ConstTrait>() {} +const fn const_trait_bounds_good<T: ConstTrait + [const] ConstTrait>() {} -const fn const_trait_bounds_bad<T: ~const ConstTrait>() {} +const fn const_trait_bounds_bad<T: [const] ConstTrait>() {} //~^ trait_duplication_in_bounds fn projections<T, U, V>() diff --git a/src/tools/clippy/tests/ui/trait_duplication_in_bounds.rs b/src/tools/clippy/tests/ui/trait_duplication_in_bounds.rs index a1a86fe058e..955562f08dc 100644 --- a/src/tools/clippy/tests/ui/trait_duplication_in_bounds.rs +++ b/src/tools/clippy/tests/ui/trait_duplication_in_bounds.rs @@ -169,9 +169,9 @@ where // #13476 #[const_trait] trait ConstTrait {} -const fn const_trait_bounds_good<T: ConstTrait + ~const ConstTrait>() {} +const fn const_trait_bounds_good<T: ConstTrait + [const] ConstTrait>() {} -const fn const_trait_bounds_bad<T: ~const ConstTrait + ~const ConstTrait>() {} +const fn const_trait_bounds_bad<T: [const] ConstTrait + [const] ConstTrait>() {} //~^ trait_duplication_in_bounds fn projections<T, U, V>() diff --git a/src/tools/clippy/tests/ui/trait_duplication_in_bounds.stderr b/src/tools/clippy/tests/ui/trait_duplication_in_bounds.stderr index d76b4e45848..ab31721ef51 100644 --- a/src/tools/clippy/tests/ui/trait_duplication_in_bounds.stderr +++ b/src/tools/clippy/tests/ui/trait_duplication_in_bounds.stderr @@ -61,8 +61,8 @@ LL | fn bad_trait_object(arg0: &(dyn Any + Send + Send)) { error: these bounds contain repeated elements --> tests/ui/trait_duplication_in_bounds.rs:174:36 | -LL | const fn const_trait_bounds_bad<T: ~const ConstTrait + ~const ConstTrait>() {} - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `~const ConstTrait` +LL | const fn const_trait_bounds_bad<T: [const] ConstTrait + [const] ConstTrait>() {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `[const] ConstTrait` error: these where clauses contain repeated elements --> tests/ui/trait_duplication_in_bounds.rs:181:8 diff --git a/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.rs b/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.rs index 6652efd9ae1..66590be3d05 100644 --- a/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.rs +++ b/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.rs @@ -21,3 +21,16 @@ fn main() { let _: String = format!("{:?}", os_str); //~ unnecessary_debug_formatting let _: String = format!("{:?}", os_string); //~ unnecessary_debug_formatting } + +#[clippy::msrv = "1.86"] +fn msrv_1_86() { + let os_str = OsStr::new("test"); + println!("{:?}", os_str); +} + +#[clippy::msrv = "1.87"] +fn msrv_1_87() { + let os_str = OsStr::new("test"); + println!("{:?}", os_str); + //~^ unnecessary_debug_formatting +} diff --git a/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.stderr b/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.stderr index 382e59b0461..f04d2d5bdc8 100644 --- a/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.stderr +++ b/src/tools/clippy/tests/ui/unnecessary_os_str_debug_formatting.stderr @@ -54,5 +54,14 @@ LL | let _: String = format!("{:?}", os_string); = help: use `Display` formatting and change this to `os_string.display()` = note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed -error: aborting due to 6 previous errors +error: unnecessary `Debug` formatting in `println!` args + --> tests/ui/unnecessary_os_str_debug_formatting.rs:34:22 + | +LL | println!("{:?}", os_str); + | ^^^^^^ + | + = help: use `Display` formatting and change this to `os_str.display()` + = note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed + +error: aborting due to 7 previous errors diff --git a/src/tools/clippy/tests/ui/wildcard_enum_match_arm.fixed b/src/tools/clippy/tests/ui/wildcard_enum_match_arm.fixed index 141ff6eb2ac..5f738a254dc 100644 --- a/src/tools/clippy/tests/ui/wildcard_enum_match_arm.fixed +++ b/src/tools/clippy/tests/ui/wildcard_enum_match_arm.fixed @@ -91,6 +91,21 @@ fn main() { } { + pub enum Enum { + A, + B, + C(u8), + D(u8, u8), + E { e: u8 }, + }; + match Enum::A { + Enum::A => (), + Enum::B | Enum::C(_) | Enum::D(..) | Enum::E { .. } => (), + //~^ wildcard_enum_match_arm + } + } + + { #![allow(clippy::manual_non_exhaustive)] pub enum Enum { A, @@ -105,3 +120,17 @@ fn main() { } } } + +fn issue15091() { + enum Foo { + A, + B, + C, + } + + match Foo::A { + Foo::A => {}, + r#type @ Foo::B | r#type @ Foo::C => {}, + //~^ wildcard_enum_match_arm + } +} diff --git a/src/tools/clippy/tests/ui/wildcard_enum_match_arm.rs b/src/tools/clippy/tests/ui/wildcard_enum_match_arm.rs index a13684e9100..4bc4bfdcb79 100644 --- a/src/tools/clippy/tests/ui/wildcard_enum_match_arm.rs +++ b/src/tools/clippy/tests/ui/wildcard_enum_match_arm.rs @@ -91,6 +91,21 @@ fn main() { } { + pub enum Enum { + A, + B, + C(u8), + D(u8, u8), + E { e: u8 }, + }; + match Enum::A { + Enum::A => (), + _ => (), + //~^ wildcard_enum_match_arm + } + } + + { #![allow(clippy::manual_non_exhaustive)] pub enum Enum { A, @@ -105,3 +120,17 @@ fn main() { } } } + +fn issue15091() { + enum Foo { + A, + B, + C, + } + + match Foo::A { + Foo::A => {}, + r#type => {}, + //~^ wildcard_enum_match_arm + } +} diff --git a/src/tools/clippy/tests/ui/wildcard_enum_match_arm.stderr b/src/tools/clippy/tests/ui/wildcard_enum_match_arm.stderr index 088c6b7b284..d0929989494 100644 --- a/src/tools/clippy/tests/ui/wildcard_enum_match_arm.stderr +++ b/src/tools/clippy/tests/ui/wildcard_enum_match_arm.stderr @@ -38,7 +38,19 @@ error: wildcard match will also match any future added variants --> tests/ui/wildcard_enum_match_arm.rs:103:13 | LL | _ => (), + | ^ help: try: `Enum::B | Enum::C(_) | Enum::D(..) | Enum::E { .. }` + +error: wildcard match will also match any future added variants + --> tests/ui/wildcard_enum_match_arm.rs:118:13 + | +LL | _ => (), | ^ help: try: `Enum::B | Enum::__Private` -error: aborting due to 6 previous errors +error: wildcard match will also match any future added variants + --> tests/ui/wildcard_enum_match_arm.rs:133:9 + | +LL | r#type => {}, + | ^^^^^^ help: try: `r#type @ Foo::B | r#type @ Foo::C` + +error: aborting due to 8 previous errors diff --git a/src/tools/clippy/tests/versioncheck.rs b/src/tools/clippy/tests/versioncheck.rs index f6fc2354ca0..b0179387b2b 100644 --- a/src/tools/clippy/tests/versioncheck.rs +++ b/src/tools/clippy/tests/versioncheck.rs @@ -27,6 +27,7 @@ fn consistent_clippy_crate_versions() { "clippy_config/Cargo.toml", "clippy_lints/Cargo.toml", "clippy_utils/Cargo.toml", + "declare_clippy_lint/Cargo.toml", ]; for path in paths { diff --git a/src/tools/clippy/triagebot.toml b/src/tools/clippy/triagebot.toml index 16557a4bebb..4f370758c00 100644 --- a/src/tools/clippy/triagebot.toml +++ b/src/tools/clippy/triagebot.toml @@ -17,6 +17,9 @@ allow-unauthenticated = [ [issue-links] +[mentions."clippy_lints/src/doc"] +cc = ["@notriddle"] + # Prevents mentions in commits to avoid users being spammed [no-mentions] diff --git a/src/tools/clippy/util/versions.py b/src/tools/clippy/util/versions.py index fee0d292df1..6e06d77a771 100755 --- a/src/tools/clippy/util/versions.py +++ b/src/tools/clippy/util/versions.py @@ -6,11 +6,11 @@ import os import sys def key(v): - if v == "master": - return sys.maxsize if v == "stable": - return sys.maxsize - 1 + return sys.maxsize if v == "beta": + return sys.maxsize - 1 + if v == "master": return sys.maxsize - 2 if v == "pre-1.29.0": return -1 diff --git a/src/tools/compiletest/src/errors.rs b/src/tools/compiletest/src/errors.rs index b5a2b7feac9..9fa26305f6b 100644 --- a/src/tools/compiletest/src/errors.rs +++ b/src/tools/compiletest/src/errors.rs @@ -16,6 +16,8 @@ pub enum ErrorKind { Suggestion, Warning, Raw, + /// Used for better recovery and diagnostics in compiletest. + Unknown, } impl ErrorKind { @@ -31,21 +33,25 @@ impl ErrorKind { /// Either the canonical uppercase string, or some additional versions for compatibility. /// FIXME: consider keeping only the canonical versions here. - pub fn from_user_str(s: &str) -> ErrorKind { - match s { + fn from_user_str(s: &str) -> Option<ErrorKind> { + Some(match s { "HELP" | "help" => ErrorKind::Help, "ERROR" | "error" => ErrorKind::Error, - // `MONO_ITEM` makes annotations in `codegen-units` tests syntactically correct, - // but those tests never use the error kind later on. - "NOTE" | "note" | "MONO_ITEM" => ErrorKind::Note, + "NOTE" | "note" => ErrorKind::Note, "SUGGESTION" => ErrorKind::Suggestion, "WARN" | "WARNING" | "warn" | "warning" => ErrorKind::Warning, "RAW" => ErrorKind::Raw, - _ => panic!( + _ => return None, + }) + } + + pub fn expect_from_user_str(s: &str) -> ErrorKind { + ErrorKind::from_user_str(s).unwrap_or_else(|| { + panic!( "unexpected diagnostic kind `{s}`, expected \ - `ERROR`, `WARN`, `NOTE`, `HELP` or `SUGGESTION`" - ), - } + `ERROR`, `WARN`, `NOTE`, `HELP`, `SUGGESTION` or `RAW`" + ) + }) } } @@ -58,6 +64,7 @@ impl fmt::Display for ErrorKind { ErrorKind::Suggestion => write!(f, "SUGGESTION"), ErrorKind::Warning => write!(f, "WARN"), ErrorKind::Raw => write!(f, "RAW"), + ErrorKind::Unknown => write!(f, "UNKNOWN"), } } } @@ -65,6 +72,7 @@ impl fmt::Display for ErrorKind { #[derive(Debug)] pub struct Error { pub line_num: Option<usize>, + pub column_num: Option<usize>, /// What kind of message we expect (e.g., warning, error, suggestion). pub kind: ErrorKind, pub msg: String, @@ -74,17 +82,6 @@ pub struct Error { pub require_annotation: bool, } -impl Error { - pub fn render_for_expected(&self) -> String { - use colored::Colorize; - format!("{: <10}line {: >3}: {}", self.kind, self.line_num_str(), self.msg.cyan()) - } - - pub fn line_num_str(&self) -> String { - self.line_num.map_or("?".to_string(), |line_num| line_num.to_string()) - } -} - /// Looks for either "//~| KIND MESSAGE" or "//~^^... KIND MESSAGE" /// The former is a "follow" that inherits its target from the preceding line; /// the latter is an "adjusts" that goes that many lines up. @@ -168,8 +165,10 @@ fn parse_expected( let rest = line[tag.end()..].trim_start(); let (kind_str, _) = rest.split_once(|c: char| c != '_' && !c.is_ascii_alphabetic()).unwrap_or((rest, "")); - let kind = ErrorKind::from_user_str(kind_str); - let untrimmed_msg = &rest[kind_str.len()..]; + let (kind, untrimmed_msg) = match ErrorKind::from_user_str(kind_str) { + Some(kind) => (kind, &rest[kind_str.len()..]), + None => (ErrorKind::Unknown, rest), + }; let msg = untrimmed_msg.strip_prefix(':').unwrap_or(untrimmed_msg).trim().to_owned(); let line_num_adjust = &captures["adjust"]; @@ -182,6 +181,7 @@ fn parse_expected( } else { (false, Some(line_num - line_num_adjust.len())) }; + let column_num = Some(tag.start() + 1); debug!( "line={:?} tag={:?} follow_prev={:?} kind={:?} msg={:?}", @@ -191,7 +191,7 @@ fn parse_expected( kind, msg ); - Some((follow_prev, Error { line_num, kind, msg, require_annotation: true })) + Some((follow_prev, Error { line_num, column_num, kind, msg, require_annotation: true })) } #[cfg(test)] diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs index e774c5e2047..0c4ef36828a 100644 --- a/src/tools/compiletest/src/executor.rs +++ b/src/tools/compiletest/src/executor.rs @@ -40,9 +40,9 @@ pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool { // In that case, the tests will effectively be run serially anyway. loop { // Spawn new test threads, up to the concurrency limit. - // FIXME(let_chains): Use a let-chain here when stable in bootstrap. - 'spawn: while running_tests.len() < concurrency { - let Some((id, test)) = fresh_tests.next() else { break 'spawn }; + while running_tests.len() < concurrency + && let Some((id, test)) = fresh_tests.next() + { listener.test_started(test); deadline_queue.push(id, test); let join_handle = spawn_test_thread(id, test, completion_tx.clone()); diff --git a/src/tools/compiletest/src/header.rs b/src/tools/compiletest/src/header.rs index 8bee9caacc9..2b203bb309c 100644 --- a/src/tools/compiletest/src/header.rs +++ b/src/tools/compiletest/src/header.rs @@ -593,7 +593,7 @@ impl TestProps { config.parse_name_value_directive(ln, DONT_REQUIRE_ANNOTATIONS) { self.dont_require_annotations - .insert(ErrorKind::from_user_str(err_kind.trim())); + .insert(ErrorKind::expect_from_user_str(err_kind.trim())); } }, ); diff --git a/src/tools/compiletest/src/json.rs b/src/tools/compiletest/src/json.rs index 6ed2b52c66d..a8e6416e56c 100644 --- a/src/tools/compiletest/src/json.rs +++ b/src/tools/compiletest/src/json.rs @@ -36,9 +36,7 @@ struct UnusedExternNotification { struct DiagnosticSpan { file_name: String, line_start: usize, - line_end: usize, column_start: usize, - column_end: usize, is_primary: bool, label: Option<String>, suggested_replacement: Option<String>, @@ -148,6 +146,7 @@ pub fn parse_output(file_name: &str, output: &str) -> Vec<Error> { Ok(diagnostic) => push_actual_errors(&mut errors, &diagnostic, &[], file_name), Err(_) => errors.push(Error { line_num: None, + column_num: None, kind: ErrorKind::Raw, msg: line.to_string(), require_annotation: false, @@ -193,25 +192,9 @@ fn push_actual_errors( // also ensure that `//~ ERROR E123` *always* works. The // assumption is that these multi-line error messages are on their // way out anyhow. - let with_code = |span: Option<&DiagnosticSpan>, text: &str| { - // FIXME(#33000) -- it'd be better to use a dedicated - // UI harness than to include the line/col number like - // this, but some current tests rely on it. - // - // Note: Do NOT include the filename. These can easily - // cause false matches where the expected message - // appears in the filename, and hence the message - // changes but the test still passes. - let span_str = match span { - Some(DiagnosticSpan { line_start, column_start, line_end, column_end, .. }) => { - format!("{line_start}:{column_start}: {line_end}:{column_end}") - } - None => format!("?:?: ?:?"), - }; - match &diagnostic.code { - Some(code) => format!("{span_str}: {text} [{}]", code.code), - None => format!("{span_str}: {text}"), - } + let with_code = |text| match &diagnostic.code { + Some(code) => format!("{text} [{}]", code.code), + None => format!("{text}"), }; // Convert multi-line messages into multiple errors. @@ -225,8 +208,9 @@ fn push_actual_errors( || Regex::new(r"aborting due to \d+ previous errors?|\d+ warnings? emitted").unwrap(); errors.push(Error { line_num: None, + column_num: None, kind, - msg: with_code(None, first_line), + msg: with_code(first_line), require_annotation: diagnostic.level != "failure-note" && !RE.get_or_init(re_init).is_match(first_line), }); @@ -234,8 +218,9 @@ fn push_actual_errors( for span in primary_spans { errors.push(Error { line_num: Some(span.line_start), + column_num: Some(span.column_start), kind, - msg: with_code(Some(span), first_line), + msg: with_code(first_line), require_annotation: true, }); } @@ -244,16 +229,18 @@ fn push_actual_errors( if primary_spans.is_empty() { errors.push(Error { line_num: None, + column_num: None, kind, - msg: with_code(None, next_line), + msg: with_code(next_line), require_annotation: false, }); } else { for span in primary_spans { errors.push(Error { line_num: Some(span.line_start), + column_num: Some(span.column_start), kind, - msg: with_code(Some(span), next_line), + msg: with_code(next_line), require_annotation: false, }); } @@ -266,6 +253,7 @@ fn push_actual_errors( for (index, line) in suggested_replacement.lines().enumerate() { errors.push(Error { line_num: Some(span.line_start + index), + column_num: Some(span.column_start), kind: ErrorKind::Suggestion, msg: line.to_string(), // Empty suggestions (suggestions to remove something) are common @@ -288,6 +276,7 @@ fn push_actual_errors( if let Some(label) = &span.label { errors.push(Error { line_num: Some(span.line_start), + column_num: Some(span.column_start), kind: ErrorKind::Note, msg: label.clone(), // Empty labels (only underlining spans) are common and do not need annotations. @@ -310,6 +299,7 @@ fn push_backtrace( if Path::new(&expansion.span.file_name) == Path::new(&file_name) { errors.push(Error { line_num: Some(expansion.span.line_start), + column_num: Some(expansion.span.column_start), kind: ErrorKind::Note, msg: format!("in this expansion of {}", expansion.macro_decl_name), require_annotation: true, diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index 42c851ea999..980e89889ab 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -11,7 +11,7 @@ use std::{env, iter, str}; use build_helper::fs::remove_and_create_dir_all; use camino::{Utf8Path, Utf8PathBuf}; -use colored::Colorize; +use colored::{Color, Colorize}; use regex::{Captures, Regex}; use tracing::*; @@ -677,9 +677,6 @@ impl<'test> TestCx<'test> { return; } - // On Windows, translate all '\' path separators to '/' - let file_name = self.testpaths.file.to_string().replace(r"\", "/"); - // On Windows, keep all '\' path separators to match the paths reported in the JSON output // from the compiler let diagnostic_file_name = if self.props.remap_src_base { @@ -704,6 +701,7 @@ impl<'test> TestCx<'test> { .map(|e| Error { msg: self.normalize_output(&e.msg, &[]), ..e }); let mut unexpected = Vec::new(); + let mut unimportant = Vec::new(); let mut found = vec![false; expected_errors.len()]; for actual_error in actual_errors { for pattern in &self.props.error_patterns { @@ -738,14 +736,9 @@ impl<'test> TestCx<'test> { && expected_kinds.contains(&actual_error.kind) && !self.props.dont_require_annotations.contains(&actual_error.kind) { - self.error(&format!( - "{}:{}: unexpected {}: '{}'", - file_name, - actual_error.line_num_str(), - actual_error.kind, - actual_error.msg - )); unexpected.push(actual_error); + } else { + unimportant.push(actual_error); } } } @@ -755,39 +748,140 @@ impl<'test> TestCx<'test> { // anything not yet found is a problem for (index, expected_error) in expected_errors.iter().enumerate() { if !found[index] { - self.error(&format!( - "{}:{}: expected {} not found: {}", - file_name, - expected_error.line_num_str(), - expected_error.kind, - expected_error.msg - )); not_found.push(expected_error); } } if !unexpected.is_empty() || !not_found.is_empty() { self.error(&format!( - "{} unexpected errors found, {} expected errors not found", + "{} unexpected diagnostics reported, {} expected diagnostics not reported", unexpected.len(), not_found.len() )); - println!("status: {}\ncommand: {}\n", proc_res.status, proc_res.cmdline); + + // Emit locations in a format that is short (relative paths) but "clickable" in editors. + // Also normalize path separators to `/`. + let file_name = self + .testpaths + .file + .strip_prefix(self.config.src_root.as_str()) + .unwrap_or(&self.testpaths.file) + .to_string() + .replace(r"\", "/"); + let line_str = |e: &Error| { + let line_num = e.line_num.map_or("?".to_string(), |line_num| line_num.to_string()); + // `file:?:NUM` may be confusing to editors and unclickable. + let opt_col_num = match e.column_num { + Some(col_num) if line_num != "?" => format!(":{col_num}"), + _ => "".to_string(), + }; + format!("{file_name}:{line_num}{opt_col_num}") + }; + let print_error = |e| println!("{}: {}: {}", line_str(e), e.kind, e.msg.cyan()); + let push_suggestion = + |suggestions: &mut Vec<_>, e: &Error, kind, line, msg, color, rank| { + let mut ret = String::new(); + if kind { + ret += &format!("{} {}", "with kind".color(color), e.kind); + } + if line { + if !ret.is_empty() { + ret.push(' '); + } + ret += &format!("{} {}", "on line".color(color), line_str(e)); + } + if msg { + if !ret.is_empty() { + ret.push(' '); + } + ret += &format!("{} {}", "with message".color(color), e.msg.cyan()); + } + suggestions.push((ret, rank)); + }; + let show_suggestions = |mut suggestions: Vec<_>, prefix: &str, color| { + // Only show suggestions with the highest rank. + suggestions.sort_by_key(|(_, rank)| *rank); + if let Some(&(_, top_rank)) = suggestions.first() { + for (suggestion, rank) in suggestions { + if rank == top_rank { + println!(" {} {suggestion}", prefix.color(color)); + } + } + } + }; + + // Fuzzy matching quality: + // - message and line / message and kind - great, suggested + // - only message - good, suggested + // - known line and kind - ok, suggested + // - only known line - meh, but suggested + // - others are not worth suggesting if !unexpected.is_empty() { - println!("{}", "--- unexpected errors (from JSON output) ---".green()); + let header = "--- reported in JSON output but not expected in test file ---"; + println!("{}", header.green()); for error in &unexpected { - println!("{}", error.render_for_expected()); + print_error(error); + let mut suggestions = Vec::new(); + for candidate in ¬_found { + let mut push_red_suggestion = |line, msg, rank| { + push_suggestion( + &mut suggestions, + candidate, + candidate.kind != error.kind, + line, + msg, + Color::Red, + rank, + ) + }; + if error.msg.contains(&candidate.msg) { + push_red_suggestion(candidate.line_num != error.line_num, false, 0); + } else if candidate.line_num.is_some() + && candidate.line_num == error.line_num + { + push_red_suggestion(false, true, 1); + } + } + + show_suggestions(suggestions, "expected", Color::Red); } println!("{}", "---".green()); } if !not_found.is_empty() { - println!("{}", "--- not found errors (from test file) ---".red()); + let header = "--- expected in test file but not reported in JSON output ---"; + println!("{}", header.red()); for error in ¬_found { - println!("{}", error.render_for_expected()); + print_error(error); + let mut suggestions = Vec::new(); + for candidate in unexpected.iter().chain(&unimportant) { + let mut push_green_suggestion = |line, msg, rank| { + push_suggestion( + &mut suggestions, + candidate, + candidate.kind != error.kind, + line, + msg, + Color::Green, + rank, + ) + }; + if candidate.msg.contains(&error.msg) { + push_green_suggestion(candidate.line_num != error.line_num, false, 0); + } else if candidate.line_num.is_some() + && candidate.line_num == error.line_num + { + push_green_suggestion(false, true, 1); + } + } + + show_suggestions(suggestions, "reported", Color::Green); } - println!("{}", "---\n".red()); + println!("{}", "---".red()); } - panic!("errors differ from expected"); + panic!( + "errors differ from expected\nstatus: {}\ncommand: {}\n", + proc_res.status, proc_res.cmdline + ); } } @@ -2073,7 +2167,6 @@ impl<'test> TestCx<'test> { println!("{}", String::from_utf8_lossy(&output.stdout)); eprintln!("{}", String::from_utf8_lossy(&output.stderr)); } else { - use colored::Colorize; eprintln!("warning: no pager configured, falling back to unified diff"); eprintln!( "help: try configuring a git pager (e.g. `delta`) with `git config --global core.pager delta`" diff --git a/src/tools/enzyme b/src/tools/enzyme -Subproject a35f4f773118ccfbd8d05102eb12a34097b1ee5 +Subproject b5098d515d5e1bd0f5470553bc0d18da9794ca8 diff --git a/src/tools/lint-docs/Cargo.toml b/src/tools/lint-docs/Cargo.toml index f1ffda75ac0..e914a2df2ba 100644 --- a/src/tools/lint-docs/Cargo.toml +++ b/src/tools/lint-docs/Cargo.toml @@ -7,7 +7,7 @@ description = "A script to extract the lint documentation for the rustc book." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rustc-literal-escaper = "0.0.2" +rustc-literal-escaper = "0.0.4" serde_json = "1.0.57" tempfile = "3.1.0" walkdir = "2.3.1" diff --git a/src/tools/lint-docs/src/lib.rs b/src/tools/lint-docs/src/lib.rs index 6bb18c2bced..b33344ca5dd 100644 --- a/src/tools/lint-docs/src/lib.rs +++ b/src/tools/lint-docs/src/lib.rs @@ -4,7 +4,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; -use rustc_literal_escaper::{Mode, unescape_unicode}; +use rustc_literal_escaper::unescape_str; use walkdir::WalkDir; mod groups; @@ -218,7 +218,7 @@ impl<'a> LintExtractor<'a> { } else if let Some(text) = line.strip_prefix("#[doc = \"") { let escaped = text.strip_suffix("\"]").unwrap(); let mut buf = String::new(); - unescape_unicode(escaped, Mode::Str, &mut |_, c| match c { + unescape_str(escaped, |_, res| match res { Ok(c) => buf.push(c), Err(err) => { assert!(!err.is_fatal(), "failed to unescape string literal") diff --git a/src/tools/miri/Cargo.lock b/src/tools/miri/Cargo.lock index 192d4f444c2..d3123caaa47 100644 --- a/src/tools/miri/Cargo.lock +++ b/src/tools/miri/Cargo.lock @@ -81,6 +81,15 @@ dependencies = [ ] [[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -113,6 +122,26 @@ dependencies = [ ] [[package]] +name = "capstone" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015ef5d5ca1743e3f94af9509ba6bd2886523cfee46e48d15c2ef5216fd4ac9a" +dependencies = [ + "capstone-sys", + "libc", +] + +[[package]] +name = "capstone-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2267cb8d16a1e4197863ec4284ffd1aec26fe7e57c58af46b02590a0235809a0" +dependencies = [ + "cc", + "libc", +] + +[[package]] name = "cargo-platform" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -151,6 +180,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] name = "chrono" version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -224,7 +259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -243,7 +278,7 @@ dependencies = [ "libc", "once_cell", "unicode-width 0.2.0", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -298,7 +333,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -314,7 +349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -334,6 +369,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -401,6 +442,25 @@ dependencies = [ ] [[package]] +name = "ipc-channel" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8251fb7bcd9ccd3725ed8deae9fe7db8e586495c9eb5b0c52e6233e5e75ea" +dependencies = [ + "bincode", + "crossbeam-channel", + "fnv", + "lazy_static", + "libc", + "mio", + "rand 0.8.5", + "serde", + "tempfile", + "uuid", + "windows", +] + +[[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -534,23 +594,39 @@ dependencies = [ ] [[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] name = "miri" version = "0.1.0" dependencies = [ "aes", "bitflags", + "capstone", "chrono", "chrono-tz", "colored", "directories", "getrandom 0.3.2", + "ipc-channel", "libc", "libffi", "libloading", "measureme", + "nix", "rand 0.9.0", "regex", "rustc_version", + "serde", "smallvec", "tempfile", "tikv-jemalloc-sys", @@ -558,6 +634,18 @@ dependencies = [ ] [[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -748,6 +836,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -757,13 +847,23 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.3", "zerocopy", ] [[package]] name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" @@ -777,6 +877,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] [[package]] name = "rand_core" @@ -879,7 +982,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -993,7 +1096,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -1148,6 +1251,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1242,6 +1354,79 @@ dependencies = [ ] [[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/src/tools/miri/Cargo.toml b/src/tools/miri/Cargo.toml index a314488bb25..0b4b8e26bb8 100644 --- a/src/tools/miri/Cargo.toml +++ b/src/tools/miri/Cargo.toml @@ -40,6 +40,12 @@ libc = "0.2" libffi = "4.0.0" libloading = "0.8" +[target.'cfg(target_os = "linux")'.dependencies] +nix = { version = "0.30.1", features = ["mman", "ptrace", "signal"] } +ipc-channel = "0.19.0" +serde = { version = "1.0.219", features = ["derive"] } +capstone = "0.13" + [dev-dependencies] ui_test = "0.29.1" colored = "2" diff --git a/src/tools/miri/README.md b/src/tools/miri/README.md index 5554c7975ff..e609fb9b6f9 100644 --- a/src/tools/miri/README.md +++ b/src/tools/miri/README.md @@ -419,6 +419,9 @@ to Miri failing to detect cases of undefined behavior in a program. Finally, the flag is **unsound** in the sense that Miri stops tracking details such as initialization and provenance on memory shared with native code, so it is easily possible to write code that has UB which is missed by Miri. +* `-Zmiri-native-lib-enable-tracing` enables the WIP detailed tracing mode for invoking native code. + Note that this flag is only meaningful on Linux systems; other Unixes (currently) do not support + tracing mode. * `-Zmiri-measureme=<name>` enables `measureme` profiling for the interpreted program. This can be used to find which parts of your program are executing slowly under Miri. The profile is written out to a file inside a directory called `<name>`, and can be processed @@ -592,7 +595,7 @@ Definite bugs found: * [Occasional memory leak in `std::mpsc` channels](https://github.com/rust-lang/rust/issues/121582) (original code in [crossbeam](https://github.com/crossbeam-rs/crossbeam/pull/1084)) * [Weak-memory-induced memory leak in Windows thread-local storage](https://github.com/rust-lang/rust/pull/124281) * [A bug in the new `RwLock::downgrade` implementation](https://rust-lang.zulipchat.com/#narrow/channel/269128-miri/topic/Miri.20error.20library.20test) (caught by Miri before it landed in the Rust repo) -* [Mockall reading unintialized memory when mocking `std::io::Read::read`, even if all expectations are satisfied](https://github.com/asomers/mockall/issues/647) (caught by Miri running Tokio's test suite) +* [Mockall reading uninitialized memory when mocking `std::io::Read::read`, even if all expectations are satisfied](https://github.com/asomers/mockall/issues/647) (caught by Miri running Tokio's test suite) * [`ReentrantLock` not correctly dealing with reuse of addresses for TLS storage of different threads](https://github.com/rust-lang/rust/pull/141248) Violations of [Stacked Borrows] found that are likely bugs (but Stacked Borrows is currently just an experiment): diff --git a/src/tools/miri/cargo-miri/src/setup.rs b/src/tools/miri/cargo-miri/src/setup.rs index b9b58c04f9e..e399f66fbc9 100644 --- a/src/tools/miri/cargo-miri/src/setup.rs +++ b/src/tools/miri/cargo-miri/src/setup.rs @@ -83,7 +83,7 @@ pub fn setup( SysrootConfig::NoStd } else { SysrootConfig::WithStd { - std_features: ["panic_unwind", "backtrace"].into_iter().map(Into::into).collect(), + std_features: ["panic-unwind", "backtrace"].into_iter().map(Into::into).collect(), } }; let cargo_cmd = { diff --git a/src/tools/miri/miri-script/src/commands.rs b/src/tools/miri/miri-script/src/commands.rs index 86362145d47..d18e9c7791f 100644 --- a/src/tools/miri/miri-script/src/commands.rs +++ b/src/tools/miri/miri-script/src/commands.rs @@ -32,6 +32,7 @@ impl MiriEnv { &mut self, quiet: bool, target: Option<impl AsRef<OsStr>>, + features: &[String], ) -> Result<PathBuf> { if let Some(miri_sysroot) = self.sh.var_os("MIRI_SYSROOT") { // Sysroot already set, use that. @@ -39,8 +40,8 @@ impl MiriEnv { } // Make sure everything is built. Also Miri itself. - self.build(".", &[], quiet)?; - self.build("cargo-miri", &[], quiet)?; + self.build(".", features, &[], quiet)?; + self.build("cargo-miri", &[], &[], quiet)?; let target_flag = if let Some(target) = &target { vec![OsStr::new("--target"), target.as_ref()] @@ -58,7 +59,7 @@ impl MiriEnv { } let mut cmd = self - .cargo_cmd("cargo-miri", "run") + .cargo_cmd("cargo-miri", "run", &[]) .arg("--quiet") .arg("--") .args(&["miri", "setup", "--print-sysroot"]) @@ -90,7 +91,9 @@ impl Command { Self::fmt(vec![])?; } if auto_clippy { - Self::clippy(vec![])?; + // no features for auto actions, see + // https://github.com/rust-lang/miri/pull/4396#discussion_r2149654845 + Self::clippy(vec![], vec![])?; } Ok(()) @@ -175,16 +178,16 @@ impl Command { } // Then run the actual command. match self { - Command::Install { flags } => Self::install(flags), - Command::Build { flags } => Self::build(flags), - Command::Check { flags } => Self::check(flags), - Command::Test { bless, flags, target, coverage } => - Self::test(bless, flags, target, coverage), - Command::Run { dep, verbose, target, edition, flags } => - Self::run(dep, verbose, target, edition, flags), - Command::Doc { flags } => Self::doc(flags), + Command::Install { features, flags } => Self::install(features, flags), + Command::Build { features, flags } => Self::build(features, flags), + Command::Check { features, flags } => Self::check(features, flags), + Command::Test { bless, target, coverage, features, flags } => + Self::test(bless, target, coverage, features, flags), + Command::Run { dep, verbose, target, edition, features, flags } => + Self::run(dep, verbose, target, edition, features, flags), + Command::Doc { features, flags } => Self::doc(features, flags), Command::Fmt { flags } => Self::fmt(flags), - Command::Clippy { flags } => Self::clippy(flags), + Command::Clippy { features, flags } => Self::clippy(features, flags), Command::Bench { target, no_install, save_baseline, load_baseline, benches } => Self::bench(target, no_install, save_baseline, load_baseline, benches), Command::Toolchain { flags } => Self::toolchain(flags), @@ -494,7 +497,7 @@ impl Command { if !no_install { // Make sure we have an up-to-date Miri installed and selected the right toolchain. - Self::install(vec![])?; + Self::install(vec![], vec![])?; } let results_json_dir = if save_baseline.is_some() || load_baseline.is_some() { Some(TempDir::new()?) @@ -601,47 +604,48 @@ impl Command { Ok(()) } - fn install(flags: Vec<String>) -> Result<()> { + fn install(features: Vec<String>, flags: Vec<String>) -> Result<()> { let e = MiriEnv::new()?; - e.install_to_sysroot(e.miri_dir.clone(), &flags)?; - e.install_to_sysroot(path!(e.miri_dir / "cargo-miri"), &flags)?; + e.install_to_sysroot(".", &features, &flags)?; + e.install_to_sysroot("cargo-miri", &[], &flags)?; Ok(()) } - fn build(flags: Vec<String>) -> Result<()> { + fn build(features: Vec<String>, flags: Vec<String>) -> Result<()> { let e = MiriEnv::new()?; - e.build(".", &flags, /* quiet */ false)?; - e.build("cargo-miri", &flags, /* quiet */ false)?; + e.build(".", &features, &flags, /* quiet */ false)?; + e.build("cargo-miri", &[], &flags, /* quiet */ false)?; Ok(()) } - fn check(flags: Vec<String>) -> Result<()> { + fn check(features: Vec<String>, flags: Vec<String>) -> Result<()> { let e = MiriEnv::new()?; - e.check(".", &flags)?; - e.check("cargo-miri", &flags)?; + e.check(".", &features, &flags)?; + e.check("cargo-miri", &[], &flags)?; Ok(()) } - fn doc(flags: Vec<String>) -> Result<()> { + fn doc(features: Vec<String>, flags: Vec<String>) -> Result<()> { let e = MiriEnv::new()?; - e.doc(".", &flags)?; - e.doc("cargo-miri", &flags)?; + e.doc(".", &features, &flags)?; + e.doc("cargo-miri", &[], &flags)?; Ok(()) } - fn clippy(flags: Vec<String>) -> Result<()> { + fn clippy(features: Vec<String>, flags: Vec<String>) -> Result<()> { let e = MiriEnv::new()?; - e.clippy(".", &flags)?; - e.clippy("cargo-miri", &flags)?; - e.clippy("miri-script", &flags)?; + e.clippy(".", &features, &flags)?; + e.clippy("cargo-miri", &[], &flags)?; + e.clippy("miri-script", &[], &flags)?; Ok(()) } fn test( bless: bool, - mut flags: Vec<String>, target: Option<String>, coverage: bool, + features: Vec<String>, + mut flags: Vec<String>, ) -> Result<()> { let mut e = MiriEnv::new()?; @@ -652,7 +656,7 @@ impl Command { } // Prepare a sysroot. (Also builds cargo-miri, which we need.) - e.build_miri_sysroot(/* quiet */ false, target.as_deref())?; + e.build_miri_sysroot(/* quiet */ false, target.as_deref(), &features)?; // Forward information to test harness. if bless { @@ -672,10 +676,10 @@ impl Command { // Then test, and let caller control flags. // Only in root project as `cargo-miri` has no tests. - e.test(".", &flags)?; + e.test(".", &features, &flags)?; if let Some(coverage) = &coverage { - coverage.show_coverage_report(&e)?; + coverage.show_coverage_report(&e, &features)?; } Ok(()) @@ -686,14 +690,17 @@ impl Command { verbose: bool, target: Option<String>, edition: Option<String>, + features: Vec<String>, flags: Vec<String>, ) -> Result<()> { let mut e = MiriEnv::new()?; // Preparation: get a sysroot, and get the miri binary. - let miri_sysroot = e.build_miri_sysroot(/* quiet */ !verbose, target.as_deref())?; - let miri_bin = - e.build_get_binary(".").context("failed to get filename of miri executable")?; + let miri_sysroot = + e.build_miri_sysroot(/* quiet */ !verbose, target.as_deref(), &features)?; + let miri_bin = e + .build_get_binary(".", &features) + .context("failed to get filename of miri executable")?; // More flags that we will pass before `flags` // (because `flags` may contain `--`). @@ -718,7 +725,7 @@ impl Command { // The basic command that executes the Miri driver. let mut cmd = if dep { // We invoke the test suite as that has all the logic for running with dependencies. - e.cargo_cmd(".", "test") + e.cargo_cmd(".", "test", &features) .args(&["--test", "ui"]) .args(quiet_flag) .arg("--") diff --git a/src/tools/miri/miri-script/src/coverage.rs b/src/tools/miri/miri-script/src/coverage.rs index 8cafcea0d16..cdf2bbb9330 100644 --- a/src/tools/miri/miri-script/src/coverage.rs +++ b/src/tools/miri/miri-script/src/coverage.rs @@ -49,7 +49,7 @@ impl CoverageReport { /// show_coverage_report will print coverage information using the artifact /// files in `self.path`. - pub fn show_coverage_report(&self, e: &MiriEnv) -> Result<()> { + pub fn show_coverage_report(&self, e: &MiriEnv, features: &[String]) -> Result<()> { let profraw_files = self.profraw_files()?; let profdata_bin = path!(e.libdir / ".." / "bin" / "llvm-profdata"); @@ -63,8 +63,9 @@ impl CoverageReport { // Create the coverage report. let cov_bin = path!(e.libdir / ".." / "bin" / "llvm-cov"); - let miri_bin = - e.build_get_binary(".").context("failed to get filename of miri executable")?; + let miri_bin = e + .build_get_binary(".", features) + .context("failed to get filename of miri executable")?; cmd!( e.sh, "{cov_bin} report --instr-profile={merged_file} --object {miri_bin} --sources src/" diff --git a/src/tools/miri/miri-script/src/main.rs b/src/tools/miri/miri-script/src/main.rs index 6aab2f79bd7..673d658cf1d 100644 --- a/src/tools/miri/miri-script/src/main.rs +++ b/src/tools/miri/miri-script/src/main.rs @@ -14,24 +14,40 @@ pub enum Command { /// Sets up the rpath such that the installed binary should work in any /// working directory. Install { + /// Pass features to cargo invocations on the "miri" crate in the root. This option does + /// **not** apply to other crates, so e.g. these features won't be used on "cargo-miri". + #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)] + features: Vec<String>, /// Flags that are passed through to `cargo install`. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] flags: Vec<String>, }, /// Build Miri. Build { + /// Pass features to cargo invocations on the "miri" crate in the root. This option does + /// **not** apply to other crates, so e.g. these features won't be used on "cargo-miri". + #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)] + features: Vec<String>, /// Flags that are passed through to `cargo build`. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] flags: Vec<String>, }, /// Check Miri. Check { + /// Pass features to cargo invocations on the "miri" crate in the root. This option does + /// **not** apply to other crates, so e.g. these features won't be used on "cargo-miri". + #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)] + features: Vec<String>, /// Flags that are passed through to `cargo check`. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] flags: Vec<String>, }, /// Check Miri with Clippy. Clippy { + /// Pass features to cargo invocations on the "miri" crate in the root. This option does + /// **not** apply to other crates, so e.g. these features won't be used on "cargo-miri". + #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)] + features: Vec<String>, /// Flags that are passed through to `cargo clippy`. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] flags: Vec<String>, @@ -47,6 +63,10 @@ pub enum Command { /// Produce coverage report. #[arg(long)] coverage: bool, + /// Pass features to cargo invocations on the "miri" crate in the root. This option does + /// **not** apply to other crates, so e.g. these features won't be used on "cargo-miri". + #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)] + features: Vec<String>, /// Flags that are passed through to the test harness. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] flags: Vec<String>, @@ -67,6 +87,10 @@ pub enum Command { /// The Rust edition. #[arg(long)] edition: Option<String>, + /// Pass features to cargo invocations on the "miri" crate in the root. This option does + /// **not** apply to other crates, so e.g. these features won't be used on "cargo-miri". + #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)] + features: Vec<String>, /// Flags that are passed through to `miri`. /// /// The flags set in `MIRIFLAGS` are added in front of these flags. @@ -75,6 +99,10 @@ pub enum Command { }, /// Build documentation. Doc { + /// Pass features to cargo invocations on the "miri" crate in the root. This option does + /// **not** apply to other crates, so e.g. these features won't be used on "cargo-miri". + #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)] + features: Vec<String>, /// Flags that are passed through to `cargo doc`. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] flags: Vec<String>, @@ -144,13 +172,13 @@ impl Command { } match self { - Self::Install { flags } - | Self::Build { flags } - | Self::Check { flags } - | Self::Doc { flags } + Self::Install { flags, .. } + | Self::Build { flags, .. } + | Self::Check { flags, .. } + | Self::Doc { flags, .. } | Self::Fmt { flags } | Self::Toolchain { flags } - | Self::Clippy { flags } + | Self::Clippy { flags, .. } | Self::Run { flags, .. } | Self::Test { flags, .. } => { flags.extend(remainder); diff --git a/src/tools/miri/miri-script/src/util.rs b/src/tools/miri/miri-script/src/util.rs index 5c2a055990f..c100cf195ba 100644 --- a/src/tools/miri/miri-script/src/util.rs +++ b/src/tools/miri/miri-script/src/util.rs @@ -26,6 +26,12 @@ pub fn flagsplit(flags: &str) -> Vec<String> { flags.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect() } +/// Turns a list of features into a list of arguments to pass to cargo invocations. +/// Each feature will go in its own argument, e.g. "--features feat1 --features feat2". +fn features_to_args(features: &[String]) -> impl IntoIterator<Item = &str> { + features.iter().flat_map(|feat| ["--features", feat]) +} + /// Some extra state we track for building Miri, such as the right RUSTFLAGS. pub struct MiriEnv { /// miri_dir is the root of the miri repository checkout we are working in. @@ -116,44 +122,70 @@ impl MiriEnv { Ok(MiriEnv { miri_dir, toolchain, sh, sysroot, cargo_extra_flags, libdir }) } - pub fn cargo_cmd(&self, crate_dir: impl AsRef<OsStr>, cmd: &str) -> Cmd<'_> { + /// Make sure the `features` you pass here exist for the specified `crate_dir`. For example, the + /// "--features" parameter of [crate::Command]s is intended only for the "miri" root crate. + pub fn cargo_cmd( + &self, + crate_dir: impl AsRef<OsStr>, + cmd: &str, + features: &[String], + ) -> Cmd<'_> { let MiriEnv { toolchain, cargo_extra_flags, .. } = self; let manifest_path = path!(self.miri_dir / crate_dir.as_ref() / "Cargo.toml"); + let features = features_to_args(features); cmd!( self.sh, - "cargo +{toolchain} {cmd} {cargo_extra_flags...} --manifest-path {manifest_path}" + "cargo +{toolchain} {cmd} {cargo_extra_flags...} --manifest-path {manifest_path} {features...}" ) } + /// Make sure the `features` you pass here exist for the specified `crate_dir`. For example, the + /// "--features" parameter of [crate::Command]s is intended only for the "miri" root crate. pub fn install_to_sysroot( &self, - path: impl AsRef<OsStr>, + crate_dir: impl AsRef<OsStr>, + features: &[String], args: impl IntoIterator<Item = impl AsRef<OsStr>>, ) -> Result<()> { let MiriEnv { sysroot, toolchain, cargo_extra_flags, .. } = self; - let path = path!(self.miri_dir / path.as_ref()); + let path = path!(self.miri_dir / crate_dir.as_ref()); + let features = features_to_args(features); // Install binaries to the miri toolchain's `sysroot` so they do not interact with other toolchains. // (Not using `cargo_cmd` as `install` is special and doesn't use `--manifest-path`.) - cmd!(self.sh, "cargo +{toolchain} install {cargo_extra_flags...} --path {path} --force --root {sysroot} {args...}").run()?; + cmd!(self.sh, "cargo +{toolchain} install {cargo_extra_flags...} --path {path} --force --root {sysroot} {features...} {args...}").run()?; Ok(()) } - pub fn build(&self, crate_dir: impl AsRef<OsStr>, args: &[String], quiet: bool) -> Result<()> { + pub fn build( + &self, + crate_dir: impl AsRef<OsStr>, + features: &[String], + args: &[String], + quiet: bool, + ) -> Result<()> { let quiet_flag = if quiet { Some("--quiet") } else { None }; // We build all targets, since building *just* the bin target doesnot include // `dev-dependencies` and that changes feature resolution. This also gets us more // parallelism in `./miri test` as we build Miri and its tests together. - let mut cmd = - self.cargo_cmd(crate_dir, "build").args(&["--all-targets"]).args(quiet_flag).args(args); + let mut cmd = self + .cargo_cmd(crate_dir, "build", features) + .args(&["--all-targets"]) + .args(quiet_flag) + .args(args); cmd.set_quiet(quiet); cmd.run()?; Ok(()) } /// Returns the path to the main crate binary. Assumes that `build` has been called before. - pub fn build_get_binary(&self, crate_dir: impl AsRef<OsStr>) -> Result<PathBuf> { - let cmd = - self.cargo_cmd(crate_dir, "build").args(&["--all-targets", "--message-format=json"]); + pub fn build_get_binary( + &self, + crate_dir: impl AsRef<OsStr>, + features: &[String], + ) -> Result<PathBuf> { + let cmd = self + .cargo_cmd(crate_dir, "build", features) + .args(&["--all-targets", "--message-format=json"]); let output = cmd.output()?; let mut bin = None; for line in output.stdout.lines() { @@ -174,23 +206,43 @@ impl MiriEnv { bin.ok_or_else(|| anyhow!("found no binary in cargo output")) } - pub fn check(&self, crate_dir: impl AsRef<OsStr>, args: &[String]) -> Result<()> { - self.cargo_cmd(crate_dir, "check").arg("--all-targets").args(args).run()?; + pub fn check( + &self, + crate_dir: impl AsRef<OsStr>, + features: &[String], + args: &[String], + ) -> Result<()> { + self.cargo_cmd(crate_dir, "check", features).arg("--all-targets").args(args).run()?; Ok(()) } - pub fn doc(&self, crate_dir: impl AsRef<OsStr>, args: &[String]) -> Result<()> { - self.cargo_cmd(crate_dir, "doc").args(args).run()?; + pub fn doc( + &self, + crate_dir: impl AsRef<OsStr>, + features: &[String], + args: &[String], + ) -> Result<()> { + self.cargo_cmd(crate_dir, "doc", features).args(args).run()?; Ok(()) } - pub fn clippy(&self, crate_dir: impl AsRef<OsStr>, args: &[String]) -> Result<()> { - self.cargo_cmd(crate_dir, "clippy").arg("--all-targets").args(args).run()?; + pub fn clippy( + &self, + crate_dir: impl AsRef<OsStr>, + features: &[String], + args: &[String], + ) -> Result<()> { + self.cargo_cmd(crate_dir, "clippy", features).arg("--all-targets").args(args).run()?; Ok(()) } - pub fn test(&self, crate_dir: impl AsRef<OsStr>, args: &[String]) -> Result<()> { - self.cargo_cmd(crate_dir, "test").args(args).run()?; + pub fn test( + &self, + crate_dir: impl AsRef<OsStr>, + features: &[String], + args: &[String], + ) -> Result<()> { + self.cargo_cmd(crate_dir, "test", features).args(args).run()?; Ok(()) } diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index 90c6e31e9d1..b2ab17fd76a 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -0cbc0764380630780a275c437260e4d4d5f28c92 +d41e12f1f4e4884c356f319b881921aa37040de5 diff --git a/src/tools/miri/src/alloc/isolated_alloc.rs b/src/tools/miri/src/alloc/isolated_alloc.rs index 3a7879f372a..a7bb9b4da75 100644 --- a/src/tools/miri/src/alloc/isolated_alloc.rs +++ b/src/tools/miri/src/alloc/isolated_alloc.rs @@ -1,5 +1,7 @@ -use std::alloc::{self, Layout}; +use std::alloc::Layout; +use std::ptr::NonNull; +use nix::sys::mman; use rustc_index::bit_set::DenseBitSet; /// How many bytes of memory each bit in the bitset represents. @@ -11,8 +13,8 @@ const COMPRESSION_FACTOR: usize = 4; pub struct IsolatedAlloc { /// Pointers to page-aligned memory that has been claimed by the allocator. /// Every pointer here must point to a page-sized allocation claimed via - /// the global allocator. These pointers are used for "small" allocations. - page_ptrs: Vec<*mut u8>, + /// mmap. These pointers are used for "small" allocations. + page_ptrs: Vec<NonNull<u8>>, /// Metadata about which bytes have been allocated on each page. The length /// of this vector must be the same as that of `page_ptrs`, and the domain /// size of the bitset must be exactly `page_size / COMPRESSION_FACTOR`. @@ -24,7 +26,7 @@ pub struct IsolatedAlloc { page_infos: Vec<DenseBitSet<usize>>, /// Pointers to multiple-page-sized allocations. These must also be page-aligned, /// with their size stored as the second element of the vector. - huge_ptrs: Vec<(*mut u8, usize)>, + huge_ptrs: Vec<(NonNull<u8>, usize)>, /// The host (not emulated) page size. page_size: usize, } @@ -52,20 +54,26 @@ impl IsolatedAlloc { Layout::from_size_align(size, align).unwrap() } - /// Returns the layout used to allocate the pages that hold small allocations. + /// For greater-than-page-sized allocations, returns the allocation size we need to request + /// including the slack we need to satisfy the alignment request. #[inline] - fn page_layout(&self) -> Layout { - Layout::from_size_align(self.page_size, self.page_size).unwrap() - } - - /// If the allocation is greater than a page, then round to the nearest page #. - #[inline] - fn huge_normalized_layout(layout: Layout, page_size: usize) -> Layout { + fn huge_normalized_layout(&self, layout: Layout) -> usize { // Allocate in page-sized chunks - let size = layout.size().next_multiple_of(page_size); + let size = layout.size().next_multiple_of(self.page_size); // And make sure the align is at least one page - let align = std::cmp::max(layout.align(), page_size); - Layout::from_size_align(size, align).unwrap() + let align = std::cmp::max(layout.align(), self.page_size); + // pg_count gives us the # of pages needed to satisfy the size. For + // align > page_size where align = n * page_size, a sufficently-aligned + // address must exist somewhere in the range of + // some_page_aligned_address..some_page_aligned_address + (n-1) * page_size + // (since if some_page_aligned_address + n * page_size is sufficently aligned, + // then so is some_page_aligned_address itself per the definition of n, so we + // can avoid using that 1 extra page). + // Thus we allocate n-1 extra pages + let pg_count = size.div_ceil(self.page_size); + let extra_pages = align.strict_div(self.page_size).saturating_sub(1); + + pg_count.strict_add(extra_pages).strict_mul(self.page_size) } /// Determined whether a given normalized (size, align) should be sent to @@ -78,7 +86,7 @@ impl IsolatedAlloc { /// Allocates memory as described in `Layout`. This memory should be deallocated /// by calling `dealloc` on this same allocator. /// - /// SAFETY: See `alloc::alloc()` + /// SAFETY: See `alloc::alloc()`. pub unsafe fn alloc(&mut self, layout: Layout) -> *mut u8 { // SAFETY: Upheld by caller unsafe { self.allocate(layout, false) } @@ -86,7 +94,7 @@ impl IsolatedAlloc { /// Same as `alloc`, but zeroes out the memory. /// - /// SAFETY: See `alloc::alloc_zeroed()` + /// SAFETY: See `alloc::alloc_zeroed()`. pub unsafe fn alloc_zeroed(&mut self, layout: Layout) -> *mut u8 { // SAFETY: Upheld by caller unsafe { self.allocate(layout, true) } @@ -95,14 +103,13 @@ impl IsolatedAlloc { /// Abstracts over the logic of `alloc_zeroed` vs `alloc`, as determined by /// the `zeroed` argument. /// - /// SAFETY: See `alloc::alloc()`, with the added restriction that `page_size` - /// corresponds to the host pagesize. + /// SAFETY: See `alloc::alloc()`. unsafe fn allocate(&mut self, layout: Layout, zeroed: bool) -> *mut u8 { let layout = IsolatedAlloc::normalized_layout(layout); if self.is_huge_alloc(&layout) { // SAFETY: Validity of `layout` upheld by caller; we checked that // the size and alignment are appropriate for being a huge alloc - unsafe { self.alloc_huge(layout, zeroed) } + unsafe { self.alloc_huge(layout) } } else { for (&mut page, pinfo) in std::iter::zip(&mut self.page_ptrs, &mut self.page_infos) { // SAFETY: The value in `self.page_size` is used to allocate @@ -132,7 +139,7 @@ impl IsolatedAlloc { unsafe fn alloc_small( page_size: usize, layout: Layout, - page: *mut u8, + page: NonNull<u8>, pinfo: &mut DenseBitSet<usize>, zeroed: bool, ) -> Option<*mut u8> { @@ -159,7 +166,7 @@ impl IsolatedAlloc { // zero out, even if we allocated more ptr.write_bytes(0, layout.size()); } - return Some(ptr); + return Some(ptr.as_ptr()); } } } @@ -167,26 +174,50 @@ impl IsolatedAlloc { } /// Expands the available memory pool by adding one page. - fn add_page(&mut self) -> (*mut u8, &mut DenseBitSet<usize>) { - // SAFETY: The system page size, which is the layout size, cannot be 0 - let page_ptr = unsafe { alloc::alloc(self.page_layout()) }; + fn add_page(&mut self) -> (NonNull<u8>, &mut DenseBitSet<usize>) { + // SAFETY: mmap is always safe to call when requesting anonymous memory + let page_ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + self.page_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ) + .cast::<u8>() + }; + assert_ne!(page_ptr.addr(), usize::MAX, "mmap failed"); // `page_infos` has to have one bit for each `COMPRESSION_FACTOR`-sized chunk of bytes in the page. - assert!(self.page_size % COMPRESSION_FACTOR == 0); + assert!(self.page_size.is_multiple_of(COMPRESSION_FACTOR)); self.page_infos.push(DenseBitSet::new_empty(self.page_size / COMPRESSION_FACTOR)); - self.page_ptrs.push(page_ptr); - (page_ptr, self.page_infos.last_mut().unwrap()) + self.page_ptrs.push(NonNull::new(page_ptr).unwrap()); + (NonNull::new(page_ptr).unwrap(), self.page_infos.last_mut().unwrap()) } /// Allocates in multiples of one page on the host system. + /// Will always be zeroed. /// /// SAFETY: Same as `alloc()`. - unsafe fn alloc_huge(&mut self, layout: Layout, zeroed: bool) -> *mut u8 { - let layout = IsolatedAlloc::huge_normalized_layout(layout, self.page_size); - // SAFETY: Upheld by caller - let ret = - unsafe { if zeroed { alloc::alloc_zeroed(layout) } else { alloc::alloc(layout) } }; - self.huge_ptrs.push((ret, layout.size())); - ret + unsafe fn alloc_huge(&mut self, layout: Layout) -> *mut u8 { + let size = self.huge_normalized_layout(layout); + // SAFETY: mmap is always safe to call when requesting anonymous memory + let ret = unsafe { + libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ) + .cast::<u8>() + }; + assert_ne!(ret.addr(), usize::MAX, "mmap failed"); + self.huge_ptrs.push((NonNull::new(ret).unwrap(), size)); + // huge_normalized_layout ensures that we've overallocated enough space + // for this to be valid. + ret.map_addr(|a| a.next_multiple_of(layout.align())) } /// Deallocates a pointer from this allocator. @@ -215,15 +246,15 @@ impl IsolatedAlloc { let page_ptr = self.page_ptrs.remove(idx); // SAFETY: We checked that there are no outstanding allocations // from us pointing to this page, and we know it was allocated - // with this layout + // in add_page as exactly a single page. unsafe { - alloc::dealloc(page_ptr, self.page_layout()); + assert_eq!(libc::munmap(page_ptr.as_ptr().cast(), self.page_size), 0); } } } } - /// Returns the index of the page that this was deallocated from + /// Returns the index of the page that this was deallocated from. /// /// SAFETY: the pointer must have been allocated with `alloc_small`. unsafe fn dealloc_small(&mut self, ptr: *mut u8, layout: Layout) -> usize { @@ -236,7 +267,7 @@ impl IsolatedAlloc { // This could be made faster if the list was sorted -- the allocator isn't fully optimized at the moment. let pinfo = std::iter::zip(&mut self.page_ptrs, &mut self.page_infos) .enumerate() - .find(|(_, (page, _))| page.addr() == page_addr); + .find(|(_, (page, _))| page.addr().get() == page_addr); let Some((idx_of_pinfo, (_, pinfo))) = pinfo else { panic!("Freeing in an unallocated page: {ptr:?}\nHolding pages {:?}", self.page_ptrs) }; @@ -252,19 +283,72 @@ impl IsolatedAlloc { /// SAFETY: Same as `dealloc()` with the added requirement that `layout` /// must ask for a size larger than the host pagesize. unsafe fn dealloc_huge(&mut self, ptr: *mut u8, layout: Layout) { - let layout = IsolatedAlloc::huge_normalized_layout(layout, self.page_size); - // Find the pointer matching in address with the one we got + let size = self.huge_normalized_layout(layout); + // Find the huge allocation containing `ptr`. let idx = self .huge_ptrs .iter() - .position(|pg| ptr.addr() == pg.0.addr()) + .position(|&(pg, size)| { + pg.addr().get() <= ptr.addr() && ptr.addr() < pg.addr().get().strict_add(size) + }) .expect("Freeing unallocated pages"); // And kick it from the list - self.huge_ptrs.remove(idx); - // SAFETY: Caller ensures validity of the layout + let (un_offset_ptr, size2) = self.huge_ptrs.remove(idx); + assert_eq!(size, size2, "got wrong layout in dealloc"); + // SAFETY: huge_ptrs contains allocations made with mmap with the size recorded there. unsafe { - alloc::dealloc(ptr, layout); + let ret = libc::munmap(un_offset_ptr.as_ptr().cast(), size); + assert_eq!(ret, 0); + } + } + + /// Returns a vector of page addresses managed by the allocator. + pub fn pages(&self) -> Vec<usize> { + let mut pages: Vec<usize> = + self.page_ptrs.iter().map(|p| p.expose_provenance().get()).collect(); + for (ptr, size) in self.huge_ptrs.iter() { + for i in 0..size / self.page_size { + pages.push(ptr.expose_provenance().get().strict_add(i * self.page_size)); + } + } + pages + } + + /// Protects all owned memory as `PROT_NONE`, preventing accesses. + /// + /// SAFETY: Accessing memory after this point will result in a segfault + /// unless it is first unprotected. + pub unsafe fn prepare_ffi(&mut self) -> Result<(), nix::errno::Errno> { + let prot = mman::ProtFlags::PROT_NONE; + unsafe { self.mprotect(prot) } + } + + /// Deprotects all owned memory by setting it to RW. Erroring here is very + /// likely unrecoverable, so it may panic if applying those permissions + /// fails. + pub fn unprep_ffi(&mut self) { + let prot = mman::ProtFlags::PROT_READ | mman::ProtFlags::PROT_WRITE; + unsafe { + self.mprotect(prot).unwrap(); + } + } + + /// Applies `prot` to every page managed by the allocator. + /// + /// SAFETY: Accessing memory in violation of the protection flags will + /// trigger a segfault. + unsafe fn mprotect(&mut self, prot: mman::ProtFlags) -> Result<(), nix::errno::Errno> { + for &pg in &self.page_ptrs { + unsafe { + mman::mprotect(pg.cast(), self.page_size, prot)?; + } + } + for &(hpg, size) in &self.huge_ptrs { + unsafe { + mman::mprotect(hpg.cast(), size.next_multiple_of(self.page_size), prot)?; + } } + Ok(()) } } @@ -347,12 +431,15 @@ mod tests { sizes.append(&mut vec![256; 12]); // Give it some multi-page ones too sizes.append(&mut vec![32 * 1024; 4]); + sizes.push(4 * 1024); // Matching aligns for the sizes let mut aligns = vec![16; 12]; aligns.append(&mut vec![256; 2]); aligns.append(&mut vec![64; 12]); aligns.append(&mut vec![4096; 4]); + // And one that requests align > page_size + aligns.push(64 * 1024); // Make sure we didn't mess up in the test itself! assert_eq!(sizes.len(), aligns.len()); diff --git a/src/tools/miri/src/alloc_addresses/reuse_pool.rs b/src/tools/miri/src/alloc_addresses/reuse_pool.rs index ab6aaed5e3e..b6cc017f772 100644 --- a/src/tools/miri/src/alloc_addresses/reuse_pool.rs +++ b/src/tools/miri/src/alloc_addresses/reuse_pool.rs @@ -129,7 +129,7 @@ impl ReusePool { let idx = rng.random_range(begin..end); // Remove it from the pool and return. let (chosen_addr, chosen_size, chosen_thread, clock) = subpool.remove(idx); - debug_assert!(chosen_size >= size && chosen_addr % align.bytes() == 0); + debug_assert!(chosen_size >= size && chosen_addr.is_multiple_of(align.bytes())); debug_assert!(cross_thread_reuse || chosen_thread == thread); // No synchronization needed if we reused from the current thread. Some((chosen_addr, if chosen_thread == thread { None } else { Some(clock) })) diff --git a/src/tools/miri/src/bin/miri.rs b/src/tools/miri/src/bin/miri.rs index d4ba7fbd6a4..92d21737ec6 100644 --- a/src/tools/miri/src/bin/miri.rs +++ b/src/tools/miri/src/bin/miri.rs @@ -227,10 +227,11 @@ impl rustc_driver::Callbacks for MiriCompilerCalls { } else { let return_code = miri::eval_entry(tcx, entry_def_id, entry_type, &config, None) .unwrap_or_else(|| { + //#[cfg(target_os = "linux")] + //miri::native_lib::register_retcode_sv(rustc_driver::EXIT_FAILURE); tcx.dcx().abort_if_errors(); rustc_driver::EXIT_FAILURE }); - std::process::exit(return_code); } @@ -295,6 +296,7 @@ impl rustc_driver::Callbacks for MiriBeRustCompilerCalls { level: SymbolExportLevel::C, kind: SymbolExportKind::Text, used: false, + rustc_std_internal_symbol: false, }, )) } else { @@ -722,6 +724,8 @@ fn main() { } else { show_error!("-Zmiri-native-lib `{}` does not exist", filename); } + } else if arg == "-Zmiri-native-lib-enable-tracing" { + miri_config.native_lib_enable_tracing = true; } else if let Some(param) = arg.strip_prefix("-Zmiri-num-cpus=") { let num_cpus = param .parse::<u32>() @@ -792,6 +796,16 @@ fn main() { debug!("rustc arguments: {:?}", rustc_args); debug!("crate arguments: {:?}", miri_config.args); + #[cfg(target_os = "linux")] + if !miri_config.native_lib.is_empty() && miri_config.native_lib_enable_tracing { + // FIXME: This should display a diagnostic / warning on error + // SAFETY: If any other threads exist at this point (namely for the ctrlc + // handler), they will not interact with anything on the main rustc/Miri + // thread in an async-signal-unsafe way such as by accessing shared + // semaphores, etc.; the handler only calls `sleep()` and `exit()`, which + // are async-signal-safe, as is accessing atomics + //let _ = unsafe { miri::native_lib::init_sv() }; + } run_compiler_and_exit( &rustc_args, &mut MiriCompilerCalls::new(miri_config, many_seeds, genmc_config), diff --git a/src/tools/miri/src/borrow_tracker/stacked_borrows/mod.rs b/src/tools/miri/src/borrow_tracker/stacked_borrows/mod.rs index bc57ba697b3..b8bcacf7c99 100644 --- a/src/tools/miri/src/borrow_tracker/stacked_borrows/mod.rs +++ b/src/tools/miri/src/borrow_tracker/stacked_borrows/mod.rs @@ -814,7 +814,7 @@ trait EvalContextPrivExt<'tcx, 'ecx>: crate::MiriInterpCxExt<'tcx> { info: RetagInfo, // diagnostics info about this retag ) -> InterpResult<'tcx, MPlaceTy<'tcx>> { let this = self.eval_context_mut(); - let size = this.size_and_align_of_mplace(place)?.map(|(size, _)| size); + let size = this.size_and_align_of_val(place)?.map(|(size, _)| size); // FIXME: If we cannot determine the size (because the unsized tail is an `extern type`), // bail out -- we cannot reasonably figure out which memory range to reborrow. // See https://github.com/rust-lang/unsafe-code-guidelines/issues/276. diff --git a/src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs b/src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs index ce8fe03ee47..a0761cb07a1 100644 --- a/src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs +++ b/src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs @@ -468,10 +468,8 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // - when `extern type` is involved we use the size of the known prefix, // - if the pointer is not reborrowed (raw pointer) then we override the size // to do a zero-length reborrow. - let reborrow_size = this - .size_and_align_of_mplace(place)? - .map(|(size, _)| size) - .unwrap_or(place.layout.size); + let reborrow_size = + this.size_and_align_of_val(place)?.map(|(size, _)| size).unwrap_or(place.layout.size); trace!("Creating new permission: {:?} with size {:?}", new_perm, reborrow_size); // This new tag is not guaranteed to actually be used. diff --git a/src/tools/miri/src/borrow_tracker/tree_borrows/unimap.rs b/src/tools/miri/src/borrow_tracker/tree_borrows/unimap.rs index dcd5a6cb023..ad0a565dfd8 100644 --- a/src/tools/miri/src/borrow_tracker/tree_borrows/unimap.rs +++ b/src/tools/miri/src/borrow_tracker/tree_borrows/unimap.rs @@ -327,7 +327,7 @@ mod tests { for i in 0..1000 { i.hash(&mut hasher); let rng = hasher.finish(); - let op = rng % 3 == 0; + let op = rng.is_multiple_of(3); let key = (rng / 2) % 50; let val = (rng / 100) % 1000; if op { diff --git a/src/tools/miri/src/concurrency/thread.rs b/src/tools/miri/src/concurrency/thread.rs index 38b5d4c0f06..c8a408fd8ac 100644 --- a/src/tools/miri/src/concurrency/thread.rs +++ b/src/tools/miri/src/concurrency/thread.rs @@ -582,88 +582,6 @@ impl<'tcx> ThreadManager<'tcx> { interp_ok(()) } - /// Mark that the active thread tries to join the thread with `joined_thread_id`. - fn join_thread( - &mut self, - joined_thread_id: ThreadId, - data_race_handler: &mut GlobalDataRaceHandler, - ) -> InterpResult<'tcx> { - if self.threads[joined_thread_id].join_status == ThreadJoinStatus::Detached { - // On Windows this corresponds to joining on a closed handle. - throw_ub_format!("trying to join a detached thread"); - } - - fn after_join<'tcx>( - threads: &mut ThreadManager<'_>, - joined_thread_id: ThreadId, - data_race_handler: &mut GlobalDataRaceHandler, - ) -> InterpResult<'tcx> { - match data_race_handler { - GlobalDataRaceHandler::None => {} - GlobalDataRaceHandler::Vclocks(data_race) => - data_race.thread_joined(threads, joined_thread_id), - GlobalDataRaceHandler::Genmc(genmc_ctx) => - genmc_ctx.handle_thread_join(threads.active_thread, joined_thread_id)?, - } - interp_ok(()) - } - - // Mark the joined thread as being joined so that we detect if other - // threads try to join it. - self.threads[joined_thread_id].join_status = ThreadJoinStatus::Joined; - if !self.threads[joined_thread_id].state.is_terminated() { - trace!( - "{:?} blocked on {:?} when trying to join", - self.active_thread, joined_thread_id - ); - // The joined thread is still running, we need to wait for it. - // Unce we get unblocked, perform the appropriate synchronization. - self.block_thread( - BlockReason::Join(joined_thread_id), - None, - callback!( - @capture<'tcx> { - joined_thread_id: ThreadId, - } - |this, unblock: UnblockKind| { - assert_eq!(unblock, UnblockKind::Ready); - after_join(&mut this.machine.threads, joined_thread_id, &mut this.machine.data_race) - } - ), - ); - } else { - // The thread has already terminated - establish happens-before - after_join(self, joined_thread_id, data_race_handler)?; - } - interp_ok(()) - } - - /// Mark that the active thread tries to exclusively join the thread with `joined_thread_id`. - /// If the thread is already joined by another thread, it will throw UB - fn join_thread_exclusive( - &mut self, - joined_thread_id: ThreadId, - data_race_handler: &mut GlobalDataRaceHandler, - ) -> InterpResult<'tcx> { - if self.threads[joined_thread_id].join_status == ThreadJoinStatus::Joined { - throw_ub_format!("trying to join an already joined thread"); - } - - if joined_thread_id == self.active_thread { - throw_ub_format!("trying to join itself"); - } - - // Sanity check `join_status`. - assert!( - self.threads - .iter() - .all(|thread| { !thread.state.is_blocked_on(BlockReason::Join(joined_thread_id)) }), - "this thread already has threads waiting for its termination" - ); - - self.join_thread(joined_thread_id, data_race_handler) - } - /// Set the name of the given thread. pub fn set_thread_name(&mut self, thread: ThreadId, new_thread_name: Vec<u8>) { self.threads[thread].thread_name = Some(new_thread_name); @@ -1114,20 +1032,102 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.machine.threads.detach_thread(thread_id, allow_terminated_joined) } - #[inline] - fn join_thread(&mut self, joined_thread_id: ThreadId) -> InterpResult<'tcx> { + /// Mark that the active thread tries to join the thread with `joined_thread_id`. + /// + /// When the join is successful (immediately, or as soon as the joined thread finishes), `success_retval` will be written to `return_dest`. + fn join_thread( + &mut self, + joined_thread_id: ThreadId, + success_retval: Scalar, + return_dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { let this = self.eval_context_mut(); - this.machine.threads.join_thread(joined_thread_id, &mut this.machine.data_race)?; + let thread_mgr = &mut this.machine.threads; + if thread_mgr.threads[joined_thread_id].join_status == ThreadJoinStatus::Detached { + // On Windows this corresponds to joining on a closed handle. + throw_ub_format!("trying to join a detached thread"); + } + + fn after_join<'tcx>( + this: &mut InterpCx<'tcx, MiriMachine<'tcx>>, + joined_thread_id: ThreadId, + success_retval: Scalar, + return_dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { + let threads = &this.machine.threads; + match &mut this.machine.data_race { + GlobalDataRaceHandler::None => {} + GlobalDataRaceHandler::Vclocks(data_race) => + data_race.thread_joined(threads, joined_thread_id), + GlobalDataRaceHandler::Genmc(genmc_ctx) => + genmc_ctx.handle_thread_join(threads.active_thread, joined_thread_id)?, + } + this.write_scalar(success_retval, return_dest)?; + interp_ok(()) + } + + // Mark the joined thread as being joined so that we detect if other + // threads try to join it. + thread_mgr.threads[joined_thread_id].join_status = ThreadJoinStatus::Joined; + if !thread_mgr.threads[joined_thread_id].state.is_terminated() { + trace!( + "{:?} blocked on {:?} when trying to join", + thread_mgr.active_thread, joined_thread_id + ); + // The joined thread is still running, we need to wait for it. + // Once we get unblocked, perform the appropriate synchronization and write the return value. + let dest = return_dest.clone(); + thread_mgr.block_thread( + BlockReason::Join(joined_thread_id), + None, + callback!( + @capture<'tcx> { + joined_thread_id: ThreadId, + dest: MPlaceTy<'tcx>, + success_retval: Scalar, + } + |this, unblock: UnblockKind| { + assert_eq!(unblock, UnblockKind::Ready); + after_join(this, joined_thread_id, success_retval, &dest) + } + ), + ); + } else { + // The thread has already terminated - establish happens-before and write the return value. + after_join(this, joined_thread_id, success_retval, return_dest)?; + } interp_ok(()) } - #[inline] - fn join_thread_exclusive(&mut self, joined_thread_id: ThreadId) -> InterpResult<'tcx> { + /// Mark that the active thread tries to exclusively join the thread with `joined_thread_id`. + /// If the thread is already joined by another thread, it will throw UB. + /// + /// When the join is successful (immediately, or as soon as the joined thread finishes), `success_retval` will be written to `return_dest`. + fn join_thread_exclusive( + &mut self, + joined_thread_id: ThreadId, + success_retval: Scalar, + return_dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { let this = self.eval_context_mut(); - this.machine - .threads - .join_thread_exclusive(joined_thread_id, &mut this.machine.data_race)?; - interp_ok(()) + let threads = &this.machine.threads.threads; + if threads[joined_thread_id].join_status == ThreadJoinStatus::Joined { + throw_ub_format!("trying to join an already joined thread"); + } + + if joined_thread_id == this.machine.threads.active_thread { + throw_ub_format!("trying to join itself"); + } + + // Sanity check `join_status`. + assert!( + threads + .iter() + .all(|thread| { !thread.state.is_blocked_on(BlockReason::Join(joined_thread_id)) }), + "this thread already has threads waiting for its termination" + ); + + this.join_thread(joined_thread_id, success_retval, return_dest) } #[inline] diff --git a/src/tools/miri/src/diagnostics.rs b/src/tools/miri/src/diagnostics.rs index 54a7c1407ea..9ecbd31c5b9 100644 --- a/src/tools/miri/src/diagnostics.rs +++ b/src/tools/miri/src/diagnostics.rs @@ -132,7 +132,9 @@ pub enum NonHaltingDiagnostic { Int2Ptr { details: bool, }, - NativeCallSharedMem, + NativeCallSharedMem { + tracing: bool, + }, WeakMemoryOutdatedLoad { ptr: Pointer, }, @@ -627,7 +629,7 @@ impl<'tcx> MiriMachine<'tcx> { RejectedIsolatedOp(_) => ("operation rejected by isolation".to_string(), DiagLevel::Warning), Int2Ptr { .. } => ("integer-to-pointer cast".to_string(), DiagLevel::Warning), - NativeCallSharedMem => + NativeCallSharedMem { .. } => ("sharing memory with a native function".to_string(), DiagLevel::Warning), ExternTypeReborrow => ("reborrow of reference to `extern type`".to_string(), DiagLevel::Warning), @@ -663,7 +665,8 @@ impl<'tcx> MiriMachine<'tcx> { ProgressReport { .. } => format!("progress report: current operation being executed is here"), Int2Ptr { .. } => format!("integer-to-pointer cast"), - NativeCallSharedMem => format!("sharing memory with a native function called via FFI"), + NativeCallSharedMem { .. } => + format!("sharing memory with a native function called via FFI"), WeakMemoryOutdatedLoad { ptr } => format!("weak memory emulation: outdated value returned from load at {ptr}"), ExternTypeReborrow => @@ -709,22 +712,41 @@ impl<'tcx> MiriMachine<'tcx> { } v } - NativeCallSharedMem => { - vec![ - note!( - "when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory" - ), - note!( - "in particular, Miri assumes that the native call initializes all memory it has access to" - ), - note!( - "Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory" - ), - note!( - "what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free" - ), - ] - } + NativeCallSharedMem { tracing } => + if *tracing { + vec![ + note!( + "when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis" + ), + note!( + "in particular, Miri assumes that the native call initializes all memory it has written to" + ), + note!( + "Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory" + ), + note!( + "what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free" + ), + note!( + "tracing memory accesses in native code is not yet fully implemented, so there can be further imprecisions beyond what is documented here" + ), + ] + } else { + vec![ + note!( + "when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory" + ), + note!( + "in particular, Miri assumes that the native call initializes all memory it has access to" + ), + note!( + "Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory" + ), + note!( + "what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free" + ), + ] + }, ExternTypeReborrow => { assert!(self.borrow_tracker.as_ref().is_some_and(|b| { matches!( diff --git a/src/tools/miri/src/eval.rs b/src/tools/miri/src/eval.rs index 7a5f96ec177..44612dae1ee 100644 --- a/src/tools/miri/src/eval.rs +++ b/src/tools/miri/src/eval.rs @@ -11,7 +11,7 @@ use rustc_abi::ExternAbi; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_hir::def::Namespace; use rustc_hir::def_id::DefId; -use rustc_middle::ty::layout::{LayoutCx, LayoutOf}; +use rustc_middle::ty::layout::LayoutCx; use rustc_middle::ty::{self, Ty, TyCtxt}; use rustc_session::config::EntryFnType; @@ -150,6 +150,8 @@ pub struct MiriConfig { pub retag_fields: RetagFields, /// The location of the shared object files to load when calling external functions pub native_lib: Vec<PathBuf>, + /// Whether to enable the new native lib tracing system. + pub native_lib_enable_tracing: bool, /// Run a garbage collector for BorTags every N basic blocks. pub gc_interval: u32, /// The number of CPUs to be reported by miri. @@ -199,6 +201,7 @@ impl Default for MiriConfig { report_progress: None, retag_fields: RetagFields::Yes, native_lib: vec![], + native_lib_enable_tracing: false, gc_interval: 10_000, num_cpus: 1, page_size: None, diff --git a/src/tools/miri/src/helpers.rs b/src/tools/miri/src/helpers.rs index 4edecc864dd..fb34600fa37 100644 --- a/src/tools/miri/src/helpers.rs +++ b/src/tools/miri/src/helpers.rs @@ -489,7 +489,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { trace!("visit_frozen(place={:?}, size={:?})", *place, size); debug_assert_eq!( size, - this.size_and_align_of_mplace(place)? + this.size_and_align_of_val(place)? .map(|(size, _)| size) .unwrap_or_else(|| place.layout.size) ); @@ -530,7 +530,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { trace!("unsafe_cell_action on {:?}", place.ptr()); // We need a size to go on. let unsafe_cell_size = this - .size_and_align_of_mplace(place)? + .size_and_align_of_val(place)? .map(|(size, _)| size) // for extern types, just cover what we can .unwrap_or_else(|| place.layout.size); diff --git a/src/tools/miri/src/intrinsics/simd.rs b/src/tools/miri/src/intrinsics/simd.rs index dbe193bdbda..e63992aa95f 100644 --- a/src/tools/miri/src/intrinsics/simd.rs +++ b/src/tools/miri/src/intrinsics/simd.rs @@ -3,7 +3,6 @@ use rand::Rng; use rustc_abi::{Endian, HasDataLayout}; use rustc_apfloat::{Float, Round}; use rustc_middle::ty::FloatTy; -use rustc_middle::ty::layout::LayoutOf; use rustc_middle::{mir, ty}; use rustc_span::{Symbol, sym}; @@ -37,6 +36,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { | "ceil" | "floor" | "round" + | "round_ties_even" | "trunc" | "fsqrt" | "fsin" @@ -72,6 +72,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { "ceil" => Op::Round(rustc_apfloat::Round::TowardPositive), "floor" => Op::Round(rustc_apfloat::Round::TowardNegative), "round" => Op::Round(rustc_apfloat::Round::NearestTiesToAway), + "round_ties_even" => Op::Round(rustc_apfloat::Round::NearestTiesToEven), "trunc" => Op::Round(rustc_apfloat::Round::TowardZero), "ctlz" => Op::Numeric(sym::ctlz), "ctpop" => Op::Numeric(sym::ctpop), diff --git a/src/tools/miri/src/lib.rs b/src/tools/miri/src/lib.rs index e96c81d5b1d..00f26a37f71 100644 --- a/src/tools/miri/src/lib.rs +++ b/src/tools/miri/src/lib.rs @@ -10,6 +10,7 @@ #![feature(variant_count)] #![feature(yeet_expr)] #![feature(nonzero_ops)] +#![cfg_attr(bootstrap, feature(nonnull_provenance))] #![feature(strict_overflow_ops)] #![feature(pointer_is_aligned_to)] #![feature(ptr_metadata)] @@ -99,6 +100,11 @@ pub use rustc_const_eval::interpret::{self, AllocMap, Provenance as _}; use rustc_middle::{bug, span_bug}; use tracing::{info, trace}; +//#[cfg(target_os = "linux")] +//pub mod native_lib { +// pub use crate::shims::{init_sv, register_retcode_sv}; +//} + // Type aliases that set the provenance parameter. pub type Pointer = interpret::Pointer<Option<machine::Provenance>>; pub type StrictPointer = interpret::Pointer<machine::Provenance>; @@ -168,7 +174,7 @@ pub const MIRI_DEFAULT_ARGS: &[&str] = &[ "-Zmir-emit-retag", "-Zmir-preserve-ub", "-Zmir-opt-level=0", - "-Zmir-enable-passes=-CheckAlignment,-CheckNull", + "-Zmir-enable-passes=-CheckAlignment,-CheckNull,-CheckEnums", // Deduplicating diagnostics means we miss events when tracking what happens during an // execution. Let's not do that. "-Zdeduplicate-diagnostics=no", diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs index b4d7db34efa..3a748c4c687 100644 --- a/src/tools/miri/src/machine.rs +++ b/src/tools/miri/src/machine.rs @@ -1056,7 +1056,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> { // What's the offset between us and the promised alignment? let distance = offset.bytes().wrapping_sub(promised_offset.bytes()); // That must also be aligned. - if distance % align.bytes() == 0 { + if distance.is_multiple_of(align.bytes()) { // All looking good! None } else { @@ -1612,7 +1612,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> { ecx.machine.since_gc += 1; // Possibly report our progress. This will point at the terminator we are about to execute. if let Some(report_progress) = ecx.machine.report_progress { - if ecx.machine.basic_block_count % u64::from(report_progress) == 0 { + if ecx.machine.basic_block_count.is_multiple_of(u64::from(report_progress)) { ecx.emit_diagnostic(NonHaltingDiagnostic::ProgressReport { block_count: ecx.machine.basic_block_count, }); diff --git a/src/tools/miri/src/shims/backtrace.rs b/src/tools/miri/src/shims/backtrace.rs index feb83ca8829..dd00b270b38 100644 --- a/src/tools/miri/src/shims/backtrace.rs +++ b/src/tools/miri/src/shims/backtrace.rs @@ -1,5 +1,4 @@ use rustc_abi::{CanonAbi, FieldIdx, Size}; -use rustc_middle::ty::layout::LayoutOf as _; use rustc_middle::ty::{self, Instance, Ty}; use rustc_span::{BytePos, Loc, Symbol, hygiene}; use rustc_target::callconv::FnAbi; diff --git a/src/tools/miri/src/shims/mod.rs b/src/tools/miri/src/shims/mod.rs index b498551ace3..f09fc546b3e 100644 --- a/src/tools/miri/src/shims/mod.rs +++ b/src/tools/miri/src/shims/mod.rs @@ -21,6 +21,8 @@ pub mod time; pub mod tls; pub use self::files::FdTable; +//#[cfg(target_os = "linux")] +//pub use self::native_lib::trace::{init_sv, register_retcode_sv}; pub use self::unix::{DirTable, EpollInterestTable}; /// What needs to be done after emulating an item (a shim or an intrinsic) is done. diff --git a/src/tools/miri/src/shims/native_lib.rs b/src/tools/miri/src/shims/native_lib/mod.rs index 030c2e36721..9c659f65e50 100644 --- a/src/tools/miri/src/shims/native_lib.rs +++ b/src/tools/miri/src/shims/native_lib/mod.rs @@ -1,4 +1,9 @@ //! Implements calling functions from a native library. + +// FIXME: disabled since it fails to build on many targets. +//#[cfg(target_os = "linux")] +//pub mod trace; + use std::ops::Deref; use libffi::high::call as ffi; @@ -8,8 +13,15 @@ use rustc_middle::mir::interpret::Pointer; use rustc_middle::ty::{self as ty, IntTy, UintTy}; use rustc_span::Symbol; +//#[cfg(target_os = "linux")] +//use self::trace::Supervisor; use crate::*; +//#[cfg(target_os = "linux")] +//type CallResult<'tcx> = InterpResult<'tcx, (ImmTy<'tcx>, Option<self::trace::messages::MemEvents>)>; +//#[cfg(not(target_os = "linux"))] +type CallResult<'tcx> = InterpResult<'tcx, (ImmTy<'tcx>, Option<!>)>; + impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {} trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { /// Call native host function and return the output as an immediate. @@ -19,71 +31,93 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { dest: &MPlaceTy<'tcx>, ptr: CodePtr, libffi_args: Vec<libffi::high::Arg<'a>>, - ) -> InterpResult<'tcx, ImmTy<'tcx>> { + ) -> CallResult<'tcx> { let this = self.eval_context_mut(); + //#[cfg(target_os = "linux")] + //let alloc = this.machine.allocator.as_ref().unwrap(); + + // SAFETY: We don't touch the machine memory past this point. + //#[cfg(target_os = "linux")] + //let (guard, stack_ptr) = unsafe { Supervisor::start_ffi(alloc) }; // Call the function (`ptr`) with arguments `libffi_args`, and obtain the return value // as the specified primitive integer type - let scalar = match dest.layout.ty.kind() { - // ints - ty::Int(IntTy::I8) => { - // Unsafe because of the call to native code. - // Because this is calling a C function it is not necessarily sound, - // but there is no way around this and we've checked as much as we can. - let x = unsafe { ffi::call::<i8>(ptr, libffi_args.as_slice()) }; - Scalar::from_i8(x) - } - ty::Int(IntTy::I16) => { - let x = unsafe { ffi::call::<i16>(ptr, libffi_args.as_slice()) }; - Scalar::from_i16(x) - } - ty::Int(IntTy::I32) => { - let x = unsafe { ffi::call::<i32>(ptr, libffi_args.as_slice()) }; - Scalar::from_i32(x) - } - ty::Int(IntTy::I64) => { - let x = unsafe { ffi::call::<i64>(ptr, libffi_args.as_slice()) }; - Scalar::from_i64(x) - } - ty::Int(IntTy::Isize) => { - let x = unsafe { ffi::call::<isize>(ptr, libffi_args.as_slice()) }; - Scalar::from_target_isize(x.try_into().unwrap(), this) - } - // uints - ty::Uint(UintTy::U8) => { - let x = unsafe { ffi::call::<u8>(ptr, libffi_args.as_slice()) }; - Scalar::from_u8(x) - } - ty::Uint(UintTy::U16) => { - let x = unsafe { ffi::call::<u16>(ptr, libffi_args.as_slice()) }; - Scalar::from_u16(x) - } - ty::Uint(UintTy::U32) => { - let x = unsafe { ffi::call::<u32>(ptr, libffi_args.as_slice()) }; - Scalar::from_u32(x) - } - ty::Uint(UintTy::U64) => { - let x = unsafe { ffi::call::<u64>(ptr, libffi_args.as_slice()) }; - Scalar::from_u64(x) - } - ty::Uint(UintTy::Usize) => { - let x = unsafe { ffi::call::<usize>(ptr, libffi_args.as_slice()) }; - Scalar::from_target_usize(x.try_into().unwrap(), this) - } - // Functions with no declared return type (i.e., the default return) - // have the output_type `Tuple([])`. - ty::Tuple(t_list) if (*t_list).deref().is_empty() => { - unsafe { ffi::call::<()>(ptr, libffi_args.as_slice()) }; - return interp_ok(ImmTy::uninit(dest.layout)); - } - ty::RawPtr(..) => { - let x = unsafe { ffi::call::<*const ()>(ptr, libffi_args.as_slice()) }; - let ptr = Pointer::new(Provenance::Wildcard, Size::from_bytes(x.addr())); - Scalar::from_pointer(ptr, this) - } - _ => throw_unsup_format!("unsupported return type for native call: {:?}", link_name), + let res = 'res: { + let scalar = match dest.layout.ty.kind() { + // ints + ty::Int(IntTy::I8) => { + // Unsafe because of the call to native code. + // Because this is calling a C function it is not necessarily sound, + // but there is no way around this and we've checked as much as we can. + let x = unsafe { ffi::call::<i8>(ptr, libffi_args.as_slice()) }; + Scalar::from_i8(x) + } + ty::Int(IntTy::I16) => { + let x = unsafe { ffi::call::<i16>(ptr, libffi_args.as_slice()) }; + Scalar::from_i16(x) + } + ty::Int(IntTy::I32) => { + let x = unsafe { ffi::call::<i32>(ptr, libffi_args.as_slice()) }; + Scalar::from_i32(x) + } + ty::Int(IntTy::I64) => { + let x = unsafe { ffi::call::<i64>(ptr, libffi_args.as_slice()) }; + Scalar::from_i64(x) + } + ty::Int(IntTy::Isize) => { + let x = unsafe { ffi::call::<isize>(ptr, libffi_args.as_slice()) }; + Scalar::from_target_isize(x.try_into().unwrap(), this) + } + // uints + ty::Uint(UintTy::U8) => { + let x = unsafe { ffi::call::<u8>(ptr, libffi_args.as_slice()) }; + Scalar::from_u8(x) + } + ty::Uint(UintTy::U16) => { + let x = unsafe { ffi::call::<u16>(ptr, libffi_args.as_slice()) }; + Scalar::from_u16(x) + } + ty::Uint(UintTy::U32) => { + let x = unsafe { ffi::call::<u32>(ptr, libffi_args.as_slice()) }; + Scalar::from_u32(x) + } + ty::Uint(UintTy::U64) => { + let x = unsafe { ffi::call::<u64>(ptr, libffi_args.as_slice()) }; + Scalar::from_u64(x) + } + ty::Uint(UintTy::Usize) => { + let x = unsafe { ffi::call::<usize>(ptr, libffi_args.as_slice()) }; + Scalar::from_target_usize(x.try_into().unwrap(), this) + } + // Functions with no declared return type (i.e., the default return) + // have the output_type `Tuple([])`. + ty::Tuple(t_list) if (*t_list).deref().is_empty() => { + unsafe { ffi::call::<()>(ptr, libffi_args.as_slice()) }; + break 'res interp_ok(ImmTy::uninit(dest.layout)); + } + ty::RawPtr(..) => { + let x = unsafe { ffi::call::<*const ()>(ptr, libffi_args.as_slice()) }; + let ptr = Pointer::new(Provenance::Wildcard, Size::from_bytes(x.addr())); + Scalar::from_pointer(ptr, this) + } + _ => + break 'res Err(err_unsup_format!( + "unsupported return type for native call: {:?}", + link_name + )) + .into(), + }; + interp_ok(ImmTy::from_scalar(scalar, dest.layout)) }; - interp_ok(ImmTy::from_scalar(scalar, dest.layout)) + + // SAFETY: We got the guard and stack pointer from start_ffi, and + // the allocator is the same + //#[cfg(target_os = "linux")] + //let events = unsafe { Supervisor::end_ffi(alloc, guard, stack_ptr) }; + //#[cfg(not(target_os = "linux"))] + let events = None; + + interp_ok((res?, events)) } /// Get the pointer to the function of the specified name in the shared object file, @@ -179,7 +213,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // The first time this happens, print a warning. if !this.machine.native_call_mem_warned.replace(true) { // Newly set, so first time we get here. - this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem); + this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem { + //#[cfg(target_os = "linux")] + //tracing: self::trace::Supervisor::is_enabled(), + //#[cfg(not(target_os = "linux"))] + tracing: false, + }); } this.expose_provenance(prov)?; @@ -196,7 +235,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { .collect::<Vec<libffi::high::Arg<'_>>>(); // Call the function and store output, depending on return type in the function signature. - let ret = this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?; + let (ret, maybe_memevents) = + this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?; + + if cfg!(target_os = "linux") + && let Some(events) = maybe_memevents + { + trace!("Registered FFI events:\n{events:#0x?}"); + } + this.write_immediate(*ret, dest)?; interp_ok(true) } diff --git a/src/tools/miri/src/shims/native_lib/trace/child.rs b/src/tools/miri/src/shims/native_lib/trace/child.rs new file mode 100644 index 00000000000..4961e875c77 --- /dev/null +++ b/src/tools/miri/src/shims/native_lib/trace/child.rs @@ -0,0 +1,251 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use ipc_channel::ipc; +use nix::sys::{ptrace, signal}; +use nix::unistd; + +use super::CALLBACK_STACK_SIZE; +use super::messages::{Confirmation, MemEvents, StartFfiInfo, TraceRequest}; +use super::parent::{ChildListener, sv_loop}; +use crate::alloc::isolated_alloc::IsolatedAlloc; + +static SUPERVISOR: std::sync::Mutex<Option<Supervisor>> = std::sync::Mutex::new(None); + +/// The main means of communication between the child and parent process, +/// allowing the former to send requests and get info from the latter. +pub struct Supervisor { + /// Sender for FFI-mode-related requests. + message_tx: ipc::IpcSender<TraceRequest>, + /// Used for synchronisation, allowing us to receive confirmation that the + /// parent process has handled the request from `message_tx`. + confirm_rx: ipc::IpcReceiver<Confirmation>, + /// Receiver for memory acceses that ocurred during the FFI call. + event_rx: ipc::IpcReceiver<MemEvents>, +} + +/// Marker representing that an error occurred during creation of the supervisor. +#[derive(Debug)] +pub struct SvInitError; + +impl Supervisor { + /// Returns `true` if the supervisor process exists, and `false` otherwise. + pub fn is_enabled() -> bool { + SUPERVISOR.lock().unwrap().is_some() + } + + /// Begins preparations for doing an FFI call. This should be called at + /// the last possible moment before entering said call. `alloc` points to + /// the allocator which handed out the memory used for this machine. + /// + /// As this locks the supervisor via a mutex, no other threads may enter FFI + /// until this one returns and its guard is dropped via `end_ffi`. The + /// pointer returned should be passed to `end_ffi` to avoid a memory leak. + /// + /// SAFETY: The resulting guard must be dropped *via `end_ffi`* immediately + /// after the desired call has concluded. + pub unsafe fn start_ffi( + alloc: &Rc<RefCell<IsolatedAlloc>>, + ) -> (std::sync::MutexGuard<'static, Option<Supervisor>>, Option<*mut [u8; CALLBACK_STACK_SIZE]>) + { + let mut sv_guard = SUPERVISOR.lock().unwrap(); + // If the supervisor is not initialised for whatever reason, fast-fail. + // This might be desired behaviour, as even on platforms where ptracing + // is not implemented it enables us to enforce that only one FFI call + // happens at a time. + let Some(sv) = sv_guard.take() else { + return (sv_guard, None); + }; + + // Get pointers to all the pages the supervisor must allow accesses in + // and prepare the callback stack. + let page_ptrs = alloc.borrow().pages(); + let raw_stack_ptr: *mut [u8; CALLBACK_STACK_SIZE] = + Box::leak(Box::new([0u8; CALLBACK_STACK_SIZE])).as_mut_ptr().cast(); + let stack_ptr = raw_stack_ptr.expose_provenance(); + let start_info = StartFfiInfo { page_ptrs, stack_ptr }; + + // SAFETY: We do not access machine memory past this point until the + // supervisor is ready to allow it. + unsafe { + if alloc.borrow_mut().prepare_ffi().is_err() { + // Don't mess up unwinding by maybe leaving the memory partly protected + alloc.borrow_mut().unprep_ffi(); + panic!("Cannot protect memory for FFI call!"); + } + } + + // Send over the info. + // NB: if we do not wait to receive a blank confirmation response, it is + // possible that the supervisor is alerted of the SIGSTOP *before* it has + // actually received the start_info, thus deadlocking! This way, we can + // enforce an ordering for these events. + sv.message_tx.send(TraceRequest::StartFfi(start_info)).unwrap(); + sv.confirm_rx.recv().unwrap(); + *sv_guard = Some(sv); + // We need to be stopped for the supervisor to be able to make certain + // modifications to our memory - simply waiting on the recv() doesn't + // count. + signal::raise(signal::SIGSTOP).unwrap(); + (sv_guard, Some(raw_stack_ptr)) + } + + /// Undoes FFI-related preparations, allowing Miri to continue as normal, then + /// gets the memory accesses and changes performed during the FFI call. Note + /// that this may include some spurious accesses done by `libffi` itself in + /// the process of executing the function call. + /// + /// SAFETY: The `sv_guard` and `raw_stack_ptr` passed must be the same ones + /// received by a prior call to `start_ffi`, and the allocator must be the + /// one passed to it also. + pub unsafe fn end_ffi( + alloc: &Rc<RefCell<IsolatedAlloc>>, + mut sv_guard: std::sync::MutexGuard<'static, Option<Supervisor>>, + raw_stack_ptr: Option<*mut [u8; CALLBACK_STACK_SIZE]>, + ) -> Option<MemEvents> { + // We can't use IPC channels here to signal that FFI mode has ended, + // since they might allocate memory which could get us stuck in a SIGTRAP + // with no easy way out! While this could be worked around, it is much + // simpler and more robust to simply use the signals which are left for + // arbitrary usage. Since this will block until we are continued by the + // supervisor, we can assume past this point that everything is back to + // normal. + signal::raise(signal::SIGUSR1).unwrap(); + + // This is safe! It just sets memory to normal expected permissions. + alloc.borrow_mut().unprep_ffi(); + + // If this is `None`, then `raw_stack_ptr` is None and does not need to + // be deallocated (and there's no need to worry about the guard, since + // it contains nothing). + let sv = sv_guard.take()?; + // SAFETY: Caller upholds that this pointer was allocated as a box with + // this type. + unsafe { + drop(Box::from_raw(raw_stack_ptr.unwrap())); + } + // On the off-chance something really weird happens, don't block forever. + let ret = sv + .event_rx + .try_recv_timeout(std::time::Duration::from_secs(5)) + .map_err(|e| { + match e { + ipc::TryRecvError::IpcError(_) => (), + ipc::TryRecvError::Empty => + eprintln!("Waiting for accesses from supervisor timed out!"), + } + }) + .ok(); + // Do *not* leave the supervisor empty, or else we might get another fork... + *sv_guard = Some(sv); + ret + } +} + +/// Initialises the supervisor process. If this function errors, then the +/// supervisor process could not be created successfully; else, the caller +/// is now the child process and can communicate via `start_ffi`/`end_ffi`, +/// receiving back events through `get_events`. +/// +/// # Safety +/// The invariants for `fork()` must be upheld by the caller. +pub unsafe fn init_sv() -> Result<(), SvInitError> { + // FIXME: Much of this could be reimplemented via the mitosis crate if we upstream the + // relevant missing bits. + + // On Linux, this will check whether ptrace is fully disabled by the Yama module. + // If Yama isn't running or we're not on Linux, we'll still error later, but + // this saves a very expensive fork call. + let ptrace_status = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope"); + if let Ok(stat) = ptrace_status { + if let Some(stat) = stat.chars().next() { + // Fast-error if ptrace is fully disabled on the system. + if stat == '3' { + return Err(SvInitError); + } + } + } + + // Initialise the supervisor if it isn't already, placing it into SUPERVISOR. + let mut lock = SUPERVISOR.lock().unwrap(); + if lock.is_some() { + return Ok(()); + } + + // Prepare the IPC channels we need. + let (message_tx, message_rx) = ipc::channel().unwrap(); + let (confirm_tx, confirm_rx) = ipc::channel().unwrap(); + let (event_tx, event_rx) = ipc::channel().unwrap(); + // SAFETY: Calling sysconf(_SC_PAGESIZE) is always safe and cannot error. + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.try_into().unwrap(); + super::parent::PAGE_SIZE.store(page_size, std::sync::atomic::Ordering::Relaxed); + + unsafe { + // TODO: Maybe use clone3() instead for better signalling of when the child exits? + // SAFETY: Caller upholds that only one thread exists. + match unistd::fork().unwrap() { + unistd::ForkResult::Parent { child } => { + // If somehow another thread does exist, prevent it from accessing the lock + // and thus breaking our safety invariants. + std::mem::forget(lock); + // The child process is free to unwind, so we won't to avoid doubly freeing + // system resources. + let init = std::panic::catch_unwind(|| { + let listener = + ChildListener { message_rx, attached: false, override_retcode: None }; + // Trace as many things as possible, to be able to handle them as needed. + let options = ptrace::Options::PTRACE_O_TRACESYSGOOD + | ptrace::Options::PTRACE_O_TRACECLONE + | ptrace::Options::PTRACE_O_TRACEFORK; + // Attach to the child process without stopping it. + match ptrace::seize(child, options) { + // Ptrace works :D + Ok(_) => { + let code = sv_loop(listener, child, event_tx, confirm_tx).unwrap_err(); + // If a return code of 0 is not explicitly given, assume something went + // wrong and return 1. + std::process::exit(code.0.unwrap_or(1)) + } + // Ptrace does not work and we failed to catch that. + Err(_) => { + // If we can't ptrace, Miri continues being the parent. + signal::kill(child, signal::SIGKILL).unwrap(); + SvInitError + } + } + }); + match init { + // The "Ok" case means that we couldn't ptrace. + Ok(e) => return Err(e), + Err(p) => { + eprintln!("Supervisor process panicked!\n{p:?}"); + std::process::exit(1); + } + } + } + unistd::ForkResult::Child => { + // Make sure we never get orphaned and stuck in SIGSTOP or similar + // SAFETY: prctl PR_SET_PDEATHSIG is always safe to call. + let ret = libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM); + assert_eq!(ret, 0); + // First make sure the parent succeeded with ptracing us! + signal::raise(signal::SIGSTOP).unwrap(); + // If we're the child process, save the supervisor info. + *lock = Some(Supervisor { message_tx, confirm_rx, event_rx }); + } + } + } + Ok(()) +} + +/// Instruct the supervisor process to return a particular code. Useful if for +/// whatever reason this code fails to be intercepted normally. In the case of +/// `abort_if_errors()` used in `bin/miri.rs`, the return code is erroneously +/// given as a 0 if this is not used. +pub fn register_retcode_sv(code: i32) { + let mut sv_guard = SUPERVISOR.lock().unwrap(); + if let Some(sv) = sv_guard.take() { + sv.message_tx.send(TraceRequest::OverrideRetcode(code)).unwrap(); + *sv_guard = Some(sv); + } +} diff --git a/src/tools/miri/src/shims/native_lib/trace/messages.rs b/src/tools/miri/src/shims/native_lib/trace/messages.rs new file mode 100644 index 00000000000..8a83dab5c09 --- /dev/null +++ b/src/tools/miri/src/shims/native_lib/trace/messages.rs @@ -0,0 +1,80 @@ +//! Houses the types that are directly sent across the IPC channels. +//! +//! The overall structure of a traced FFI call, from the child process's POV, is +//! as follows: +//! ``` +//! message_tx.send(TraceRequest::StartFfi); +//! confirm_rx.recv(); +//! raise(SIGSTOP); +//! /* do ffi call */ +//! raise(SIGUSR1); // morally equivalent to some kind of "TraceRequest::EndFfi" +//! let events = event_rx.recv(); +//! ``` +//! `TraceRequest::OverrideRetcode` can be sent at any point in the above, including +//! before or after all of them. +//! +//! NB: sending these events out of order, skipping steps, etc. will result in +//! unspecified behaviour from the supervisor process, so use the abstractions +//! in `super::child` (namely `start_ffi()` and `end_ffi()`) to handle this. It is +//! trivially easy to cause a deadlock or crash by messing this up! + +use std::ops::Range; + +/// An IPC request sent by the child process to the parent. +/// +/// The sender for this channel should live on the child process. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub enum TraceRequest { + /// Requests that tracing begins. Following this being sent, the child must + /// wait to receive a `Confirmation` on the respective channel and then + /// `raise(SIGSTOP)`. + /// + /// To avoid possible issues while allocating memory for IPC channels, ending + /// the tracing is instead done via `raise(SIGUSR1)`. + StartFfi(StartFfiInfo), + /// Manually overrides the code that the supervisor will return upon exiting. + /// Once set, it is permanent. This can be called again to change the value. + OverrideRetcode(i32), +} + +/// Information needed to begin tracing. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct StartFfiInfo { + /// A vector of page addresses. These should have been automatically obtained + /// with `IsolatedAlloc::pages` and prepared with `IsolatedAlloc::prepare_ffi`. + pub page_ptrs: Vec<usize>, + /// The address of an allocation that can serve as a temporary stack. + /// This should be a leaked `Box<[u8; CALLBACK_STACK_SIZE]>` cast to an int. + pub stack_ptr: usize, +} + +/// A marker type confirming that the supervisor has received the request to begin +/// tracing and is now waiting for a `SIGSTOP`. +/// +/// The sender for this channel should live on the parent process. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct Confirmation; + +/// The final results of an FFI trace, containing every relevant event detected +/// by the tracer. Sent by the supervisor after receiving a `SIGUSR1` signal. +/// +/// The sender for this channel should live on the parent process. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct MemEvents { + /// An ordered list of memory accesses that occurred. These should be assumed + /// to be overcautious; that is, if the size of an access is uncertain it is + /// pessimistically rounded up, and if the type (read/write/both) is uncertain + /// it is reported as whatever would be safest to assume; i.e. a read + maybe-write + /// becomes a read + write, etc. + pub acc_events: Vec<AccessEvent>, +} + +/// A single memory access, conservatively overestimated +/// in case of ambiguity. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub enum AccessEvent { + /// A read may have occurred on no more than the specified address range. + Read(Range<usize>), + /// A write may have occurred on no more than the specified address range. + Write(Range<usize>), +} diff --git a/src/tools/miri/src/shims/native_lib/trace/mod.rs b/src/tools/miri/src/shims/native_lib/trace/mod.rs new file mode 100644 index 00000000000..174b06b3ac5 --- /dev/null +++ b/src/tools/miri/src/shims/native_lib/trace/mod.rs @@ -0,0 +1,8 @@ +mod child; +pub mod messages; +mod parent; + +pub use self::child::{Supervisor, init_sv, register_retcode_sv}; + +/// The size of the temporary stack we use for callbacks that the server executes in the client. +const CALLBACK_STACK_SIZE: usize = 1024; diff --git a/src/tools/miri/src/shims/native_lib/trace/parent.rs b/src/tools/miri/src/shims/native_lib/trace/parent.rs new file mode 100644 index 00000000000..dfb0b35da69 --- /dev/null +++ b/src/tools/miri/src/shims/native_lib/trace/parent.rs @@ -0,0 +1,660 @@ +use std::sync::atomic::{AtomicPtr, AtomicUsize}; + +use ipc_channel::ipc; +use nix::sys::{ptrace, signal, wait}; +use nix::unistd; + +use super::CALLBACK_STACK_SIZE; +use super::messages::{AccessEvent, Confirmation, MemEvents, StartFfiInfo, TraceRequest}; + +/// The flags to use when calling `waitid()`. +/// Since bitwise or on the nix version of these flags is implemented as a trait, +/// this cannot be const directly so we do it this way. +const WAIT_FLAGS: wait::WaitPidFlag = + wait::WaitPidFlag::from_bits_truncate(libc::WUNTRACED | libc::WEXITED); + +/// Arch-specific maximum size a single access might perform. x86 value is set +/// assuming nothing bigger than AVX-512 is available. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +const ARCH_MAX_ACCESS_SIZE: usize = 64; +/// The largest arm64 simd instruction operates on 16 bytes. +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +const ARCH_MAX_ACCESS_SIZE: usize = 16; + +/// The default word size on a given platform, in bytes. +#[cfg(any(target_arch = "x86", target_arch = "arm"))] +const ARCH_WORD_SIZE: usize = 4; +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +const ARCH_WORD_SIZE: usize = 8; + +/// The address of the page set to be edited, initialised to a sentinel null +/// pointer. +static PAGE_ADDR: AtomicPtr<u8> = AtomicPtr::new(std::ptr::null_mut()); +/// The host pagesize, initialised to a sentinel zero value. +pub static PAGE_SIZE: AtomicUsize = AtomicUsize::new(0); +/// How many consecutive pages to unprotect. 1 by default, unlikely to be set +/// higher than 2. +static PAGE_COUNT: AtomicUsize = AtomicUsize::new(1); + +/// Allows us to get common arguments from the `user_regs_t` across architectures. +/// Normally this would land us ABI hell, but thankfully all of our usecases +/// consist of functions with a small number of register-sized integer arguments. +/// See <https://man7.org/linux/man-pages/man2/syscall.2.html> for sources. +trait ArchIndependentRegs { + /// Gets the address of the instruction pointer. + fn ip(&self) -> usize; + /// Set the instruction pointer; remember to also set the stack pointer, or + /// else the stack might get messed up! + fn set_ip(&mut self, ip: usize); + /// Set the stack pointer, ideally to a zeroed-out area. + fn set_sp(&mut self, sp: usize); +} + +// It's fine / desirable behaviour for values to wrap here, we care about just +// preserving the bit pattern. +#[cfg(target_arch = "x86_64")] +#[expect(clippy::as_conversions)] +#[rustfmt::skip] +impl ArchIndependentRegs for libc::user_regs_struct { + #[inline] + fn ip(&self) -> usize { self.rip as _ } + #[inline] + fn set_ip(&mut self, ip: usize) { self.rip = ip as _ } + #[inline] + fn set_sp(&mut self, sp: usize) { self.rsp = sp as _ } +} + +#[cfg(target_arch = "x86")] +#[expect(clippy::as_conversions)] +#[rustfmt::skip] +impl ArchIndependentRegs for libc::user_regs_struct { + #[inline] + fn ip(&self) -> usize { self.eip as _ } + #[inline] + fn set_ip(&mut self, ip: usize) { self.eip = ip as _ } + #[inline] + fn set_sp(&mut self, sp: usize) { self.esp = sp as _ } +} + +#[cfg(target_arch = "aarch64")] +#[expect(clippy::as_conversions)] +#[rustfmt::skip] +impl ArchIndependentRegs for libc::user_regs_struct { + #[inline] + fn ip(&self) -> usize { self.pc as _ } + #[inline] + fn set_ip(&mut self, ip: usize) { self.pc = ip as _ } + #[inline] + fn set_sp(&mut self, sp: usize) { self.sp = sp as _ } +} + +/// A unified event representing something happening on the child process. Wraps +/// `nix`'s `WaitStatus` and our custom signals so it can all be done with one +/// `match` statement. +pub enum ExecEvent { + /// Child process requests that we begin monitoring it. + Start(StartFfiInfo), + /// Child requests that we stop monitoring and pass over the events we + /// detected. + End, + /// The child process with the specified pid was stopped by the given signal. + Status(unistd::Pid, signal::Signal), + /// The child process with the specified pid entered or existed a syscall. + Syscall(unistd::Pid), + /// A child process exited or was killed; if we have a return code, it is + /// specified. + Died(Option<i32>), +} + +/// A listener for the FFI start info channel along with relevant state. +pub struct ChildListener { + /// The matching channel for the child's `Supervisor` struct. + pub message_rx: ipc::IpcReceiver<TraceRequest>, + /// Whether an FFI call is currently ongoing. + pub attached: bool, + /// If `Some`, overrides the return code with the given value. + pub override_retcode: Option<i32>, +} + +impl Iterator for ChildListener { + type Item = ExecEvent; + + // Allows us to monitor the child process by just iterating over the listener. + // NB: This should never return None! + fn next(&mut self) -> Option<Self::Item> { + // Do not block if the child has nothing to report for `waitid`. + let opts = WAIT_FLAGS | wait::WaitPidFlag::WNOHANG; + loop { + // Listen to any child, not just the main one. Important if we want + // to allow the C code to fork further, along with being a bit of + // defensive programming since Linux sometimes assigns threads of + // the same process different PIDs with unpredictable rules... + match wait::waitid(wait::Id::All, opts) { + Ok(stat) => + match stat { + // Child exited normally with a specific code set. + wait::WaitStatus::Exited(_, code) => { + let code = self.override_retcode.unwrap_or(code); + return Some(ExecEvent::Died(Some(code))); + } + // Child was killed by a signal, without giving a code. + wait::WaitStatus::Signaled(_, _, _) => + return Some(ExecEvent::Died(self.override_retcode)), + // Child entered a syscall. Since we're always technically + // tracing, only pass this along if we're actively + // monitoring the child. + wait::WaitStatus::PtraceSyscall(pid) => + if self.attached { + return Some(ExecEvent::Syscall(pid)); + }, + // Child with the given pid was stopped by the given signal. + // It's somewhat dubious when this is returned instead of + // WaitStatus::Stopped, but for our purposes they are the + // same thing. + wait::WaitStatus::PtraceEvent(pid, signal, _) => + if self.attached { + // This is our end-of-FFI signal! + if signal == signal::SIGUSR1 { + self.attached = false; + return Some(ExecEvent::End); + } else { + return Some(ExecEvent::Status(pid, signal)); + } + } else { + // Just pass along the signal. + ptrace::cont(pid, signal).unwrap(); + }, + // Child was stopped at the given signal. Same logic as for + // WaitStatus::PtraceEvent. + wait::WaitStatus::Stopped(pid, signal) => + if self.attached { + if signal == signal::SIGUSR1 { + self.attached = false; + return Some(ExecEvent::End); + } else { + return Some(ExecEvent::Status(pid, signal)); + } + } else { + ptrace::cont(pid, signal).unwrap(); + }, + _ => (), + }, + // This case should only trigger if all children died and we + // somehow missed that, but it's best we not allow any room + // for deadlocks. + Err(_) => return Some(ExecEvent::Died(None)), + } + + // Similarly, do a non-blocking poll of the IPC channel. + if let Ok(req) = self.message_rx.try_recv() { + match req { + TraceRequest::StartFfi(info) => + // Should never trigger - but better to panic explicitly than deadlock! + if self.attached { + panic!("Attempting to begin FFI multiple times!"); + } else { + self.attached = true; + return Some(ExecEvent::Start(info)); + }, + TraceRequest::OverrideRetcode(code) => self.override_retcode = Some(code), + } + } + + // Not ideal, but doing anything else might sacrifice performance. + std::thread::yield_now(); + } + } +} + +/// An error came up while waiting on the child process to do something. +/// It likely died, with this return code if we have one. +#[derive(Debug)] +pub struct ExecEnd(pub Option<i32>); + +/// This is the main loop of the supervisor process. It runs in a separate +/// process from the rest of Miri (but because we fork, addresses for anything +/// created before the fork - like statics - are the same). +pub fn sv_loop( + listener: ChildListener, + init_pid: unistd::Pid, + event_tx: ipc::IpcSender<MemEvents>, + confirm_tx: ipc::IpcSender<Confirmation>, +) -> Result<!, ExecEnd> { + // Get the pagesize set and make sure it isn't still on the zero sentinel value! + let page_size = PAGE_SIZE.load(std::sync::atomic::Ordering::Relaxed); + assert_ne!(page_size, 0); + + // Things that we return to the child process. + let mut acc_events = Vec::new(); + + // Memory allocated for the MiriMachine. + let mut ch_pages = Vec::new(); + let mut ch_stack = None; + + // An instance of the Capstone disassembler, so we don't spawn one on every access. + let cs = get_disasm(); + + // The pid of the last process we interacted with, used by default if we don't have a + // reason to use a different one. + let mut curr_pid = init_pid; + + // There's an initial sigstop we need to deal with. + wait_for_signal(Some(curr_pid), signal::SIGSTOP, false)?; + ptrace::cont(curr_pid, None).unwrap(); + + for evt in listener { + match evt { + // start_ffi was called by the child, so prep memory. + ExecEvent::Start(ch_info) => { + // All the pages that the child process is "allowed to" access. + ch_pages = ch_info.page_ptrs; + // And the temporary callback stack it allocated for us to use later. + ch_stack = Some(ch_info.stack_ptr); + + // We received the signal and are no longer in the main listener loop, + // so we can let the child move on to the end of start_ffi where it will + // raise a SIGSTOP. We need it to be signal-stopped *and waited for* in + // order to do most ptrace operations! + confirm_tx.send(Confirmation).unwrap(); + // We can't trust simply calling `Pid::this()` in the child process to give the right + // PID for us, so we get it this way. + curr_pid = wait_for_signal(None, signal::SIGSTOP, false).unwrap(); + + ptrace::syscall(curr_pid, None).unwrap(); + } + // end_ffi was called by the child. + ExecEvent::End => { + // Hand over the access info we traced. + event_tx.send(MemEvents { acc_events }).unwrap(); + // And reset our values. + acc_events = Vec::new(); + ch_stack = None; + + // No need to monitor syscalls anymore, they'd just be ignored. + ptrace::cont(curr_pid, None).unwrap(); + } + // Child process was stopped by a signal + ExecEvent::Status(pid, signal) => + match signal { + // If it was a segfault, check if it was an artificial one + // caused by it trying to access the MiriMachine memory. + signal::SIGSEGV => + handle_segfault( + pid, + &ch_pages, + ch_stack.unwrap(), + page_size, + &cs, + &mut acc_events, + )?, + // Something weird happened. + _ => { + eprintln!("Process unexpectedly got {signal}; continuing..."); + // In case we're not tracing + if ptrace::syscall(pid, None).is_err() { + // If *this* fails too, something really weird happened + // and it's probably best to just panic. + signal::kill(pid, signal::SIGCONT).unwrap(); + } + } + }, + // Child entered a syscall; we wait for exits inside of this, so it + // should never trigger on return from a syscall we care about. + ExecEvent::Syscall(pid) => { + ptrace::syscall(pid, None).unwrap(); + } + ExecEvent::Died(code) => { + return Err(ExecEnd(code)); + } + } + } + + unreachable!() +} + +/// Spawns a Capstone disassembler for the host architecture. +#[rustfmt::skip] +fn get_disasm() -> capstone::Capstone { + use capstone::prelude::*; + let cs_pre = Capstone::new(); + { + #[cfg(target_arch = "x86_64")] + {cs_pre.x86().mode(arch::x86::ArchMode::Mode64)} + #[cfg(target_arch = "x86")] + {cs_pre.x86().mode(arch::x86::ArchMode::Mode32)} + #[cfg(target_arch = "aarch64")] + {cs_pre.arm64().mode(arch::arm64::ArchMode::Arm)} + #[cfg(target_arch = "arm")] + {cs_pre.arm().mode(arch::arm::ArchMode::Arm)} + } + .detail(true) + .build() + .unwrap() +} + +/// Waits for `wait_signal`. If `init_cont`, it will first do a `ptrace::cont`. +/// We want to avoid that in some cases, like at the beginning of FFI. +/// +/// If `pid` is `None`, only one wait will be done and `init_cont` should be false. +fn wait_for_signal( + pid: Option<unistd::Pid>, + wait_signal: signal::Signal, + init_cont: bool, +) -> Result<unistd::Pid, ExecEnd> { + if init_cont { + ptrace::cont(pid.unwrap(), None).unwrap(); + } + // Repeatedly call `waitid` until we get the signal we want, or the process dies. + loop { + let wait_id = match pid { + Some(pid) => wait::Id::Pid(pid), + None => wait::Id::All, + }; + let stat = wait::waitid(wait_id, WAIT_FLAGS).map_err(|_| ExecEnd(None))?; + let (signal, pid) = match stat { + // Report the cause of death, if we know it. + wait::WaitStatus::Exited(_, code) => { + return Err(ExecEnd(Some(code))); + } + wait::WaitStatus::Signaled(_, _, _) => return Err(ExecEnd(None)), + wait::WaitStatus::Stopped(pid, signal) => (signal, pid), + wait::WaitStatus::PtraceEvent(pid, signal, _) => (signal, pid), + // This covers PtraceSyscall and variants that are impossible with + // the flags set (e.g. WaitStatus::StillAlive). + _ => { + ptrace::cont(pid.unwrap(), None).unwrap(); + continue; + } + }; + if signal == wait_signal { + return Ok(pid); + } else { + ptrace::cont(pid, signal).map_err(|_| ExecEnd(None))?; + } + } +} + +/// Grabs the access that caused a segfault and logs it down if it's to our memory, +/// or kills the child and returns the appropriate error otherwise. +fn handle_segfault( + pid: unistd::Pid, + ch_pages: &[usize], + ch_stack: usize, + page_size: usize, + cs: &capstone::Capstone, + acc_events: &mut Vec<AccessEvent>, +) -> Result<(), ExecEnd> { + /// This is just here to not pollute the main namespace with `capstone::prelude::*`. + #[inline] + fn capstone_disassemble( + instr: &[u8], + addr: usize, + cs: &capstone::Capstone, + acc_events: &mut Vec<AccessEvent>, + ) -> capstone::CsResult<()> { + use capstone::prelude::*; + + // The arch_detail is what we care about, but it relies on these temporaries + // that we can't drop. 0x1000 is the default base address for Captsone, and + // we're expecting 1 instruction. + let insns = cs.disasm_count(instr, 0x1000, 1)?; + let ins_detail = cs.insn_detail(&insns[0])?; + let arch_detail = ins_detail.arch_detail(); + + for op in arch_detail.operands() { + match op { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + arch::ArchOperand::X86Operand(x86_operand) => { + match x86_operand.op_type { + // We only care about memory accesses + arch::x86::X86OperandType::Mem(_) => { + let push = addr..addr.strict_add(usize::from(x86_operand.size)); + // It's called a "RegAccessType" but it also applies to memory + let acc_ty = x86_operand.access.unwrap(); + if acc_ty.is_readable() { + acc_events.push(AccessEvent::Read(push.clone())); + } + if acc_ty.is_writable() { + acc_events.push(AccessEvent::Write(push)); + } + } + _ => (), + } + } + #[cfg(target_arch = "aarch64")] + arch::ArchOperand::Arm64Operand(arm64_operand) => { + // Annoyingly, we don't always get the size here, so just be pessimistic for now. + match arm64_operand.op_type { + arch::arm64::Arm64OperandType::Mem(_) => { + // B = 1 byte, H = 2 bytes, S = 4 bytes, D = 8 bytes, Q = 16 bytes. + let size = match arm64_operand.vas { + // Not an fp/simd instruction. + arch::arm64::Arm64Vas::ARM64_VAS_INVALID => ARCH_WORD_SIZE, + // 1 byte. + arch::arm64::Arm64Vas::ARM64_VAS_1B => 1, + // 2 bytes. + arch::arm64::Arm64Vas::ARM64_VAS_1H => 2, + // 4 bytes. + arch::arm64::Arm64Vas::ARM64_VAS_4B + | arch::arm64::Arm64Vas::ARM64_VAS_2H + | arch::arm64::Arm64Vas::ARM64_VAS_1S => 4, + // 8 bytes. + arch::arm64::Arm64Vas::ARM64_VAS_8B + | arch::arm64::Arm64Vas::ARM64_VAS_4H + | arch::arm64::Arm64Vas::ARM64_VAS_2S + | arch::arm64::Arm64Vas::ARM64_VAS_1D => 8, + // 16 bytes. + arch::arm64::Arm64Vas::ARM64_VAS_16B + | arch::arm64::Arm64Vas::ARM64_VAS_8H + | arch::arm64::Arm64Vas::ARM64_VAS_4S + | arch::arm64::Arm64Vas::ARM64_VAS_2D + | arch::arm64::Arm64Vas::ARM64_VAS_1Q => 16, + }; + let push = addr..addr.strict_add(size); + // FIXME: This now has access type info in the latest + // git version of capstone because this pissed me off + // and I added it. Change this when it updates. + acc_events.push(AccessEvent::Read(push.clone())); + acc_events.push(AccessEvent::Write(push)); + } + _ => (), + } + } + #[cfg(target_arch = "arm")] + arch::ArchOperand::ArmOperand(arm_operand) => + match arm_operand.op_type { + arch::arm::ArmOperandType::Mem(_) => { + // We don't get info on the size of the access, but + // we're at least told if it's a vector instruction. + let size = if arm_operand.vector_index.is_some() { + ARCH_MAX_ACCESS_SIZE + } else { + ARCH_WORD_SIZE + }; + let push = addr..addr.strict_add(size); + let acc_ty = arm_operand.access.unwrap(); + if acc_ty.is_readable() { + acc_events.push(AccessEvent::Read(push.clone())); + } + if acc_ty.is_writable() { + acc_events.push(AccessEvent::Write(push)); + } + } + _ => (), + }, + _ => unimplemented!(), + } + } + + Ok(()) + } + + // Get information on what caused the segfault. This contains the address + // that triggered it. + let siginfo = ptrace::getsiginfo(pid).unwrap(); + // All x86, ARM, etc. instructions only have at most one memory operand + // (thankfully!) + // SAFETY: si_addr is safe to call. + let addr = unsafe { siginfo.si_addr().addr() }; + let page_addr = addr.strict_sub(addr.strict_rem(page_size)); + + if !ch_pages.iter().any(|pg| (*pg..pg.strict_add(page_size)).contains(&addr)) { + // This was a real segfault (not one of the Miri memory pages), so print some debug info and + // quit. + let regs = ptrace::getregs(pid).unwrap(); + eprintln!("Segfault occurred during FFI at {addr:#018x}"); + eprintln!("Expected access on pages: {ch_pages:#018x?}"); + eprintln!("Register dump: {regs:#x?}"); + ptrace::kill(pid).unwrap(); + return Err(ExecEnd(None)); + } + + // Overall structure: + // - Get the address that caused the segfault + // - Unprotect the memory: we force the child to execute `mempr_off`, passing parameters via + // global atomic variables. This is what we use the temporary callback stack for. + // - Step 1 instruction + // - Parse executed code to estimate size & type of access + // - Reprotect the memory by executing `mempr_on` in the child. + // - Continue + + // Ensure the stack is properly zeroed out! + for a in (ch_stack..ch_stack.strict_add(CALLBACK_STACK_SIZE)).step_by(ARCH_WORD_SIZE) { + ptrace::write(pid, std::ptr::with_exposed_provenance_mut(a), 0).unwrap(); + } + + // Guard against both architectures with upwards and downwards-growing stacks. + let stack_ptr = ch_stack.strict_add(CALLBACK_STACK_SIZE / 2); + let regs_bak = ptrace::getregs(pid).unwrap(); + let mut new_regs = regs_bak; + let ip_prestep = regs_bak.ip(); + + // Move the instr ptr into the deprotection code. + #[expect(clippy::as_conversions)] + new_regs.set_ip(mempr_off as usize); + // Don't mess up the stack by accident! + new_regs.set_sp(stack_ptr); + + // Modify the PAGE_ADDR global on the child process to point to the page + // that we want unprotected. + ptrace::write( + pid, + (&raw const PAGE_ADDR).cast_mut().cast(), + libc::c_long::try_from(page_addr).unwrap(), + ) + .unwrap(); + + // Check if we also own the next page, and if so unprotect it in case + // the access spans the page boundary. + let flag = if ch_pages.contains(&page_addr.strict_add(page_size)) { 2 } else { 1 }; + ptrace::write(pid, (&raw const PAGE_COUNT).cast_mut().cast(), flag).unwrap(); + + ptrace::setregs(pid, new_regs).unwrap(); + + // Our mempr_* functions end with a raise(SIGSTOP). + wait_for_signal(Some(pid), signal::SIGSTOP, true)?; + + // Step 1 instruction. + ptrace::setregs(pid, regs_bak).unwrap(); + ptrace::step(pid, None).unwrap(); + // Don't use wait_for_signal here since 1 instruction doesn't give room + // for any uncertainty + we don't want it `cont()`ing randomly by accident + // Also, don't let it continue with unprotected memory if something errors! + let _ = wait::waitid(wait::Id::Pid(pid), WAIT_FLAGS).map_err(|_| ExecEnd(None))?; + + // Zero out again to be safe + for a in (ch_stack..ch_stack.strict_add(CALLBACK_STACK_SIZE)).step_by(ARCH_WORD_SIZE) { + ptrace::write(pid, std::ptr::with_exposed_provenance_mut(a), 0).unwrap(); + } + + // Save registers and grab the bytes that were executed. This would + // be really nasty if it was a jump or similar but those thankfully + // won't do memory accesses and so can't trigger this! + let regs_bak = ptrace::getregs(pid).unwrap(); + new_regs = regs_bak; + let ip_poststep = regs_bak.ip(); + // We need to do reads/writes in word-sized chunks. + let diff = (ip_poststep.strict_sub(ip_prestep)).div_ceil(ARCH_WORD_SIZE); + let instr = (ip_prestep..ip_prestep.strict_add(diff)).fold(vec![], |mut ret, ip| { + // This only needs to be a valid pointer in the child process, not ours. + ret.append( + &mut ptrace::read(pid, std::ptr::without_provenance_mut(ip)) + .unwrap() + .to_ne_bytes() + .to_vec(), + ); + ret + }); + + // Now figure out the size + type of access and log it down. + // This will mark down e.g. the same area being read multiple times, + // since it's more efficient to compress the accesses at the end. + if capstone_disassemble(&instr, addr, cs, acc_events).is_err() { + // Read goes first because we need to be pessimistic. + acc_events.push(AccessEvent::Read(addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE))); + acc_events.push(AccessEvent::Write(addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE))); + } + + // Reprotect everything and continue. + #[expect(clippy::as_conversions)] + new_regs.set_ip(mempr_on as usize); + new_regs.set_sp(stack_ptr); + ptrace::setregs(pid, new_regs).unwrap(); + wait_for_signal(Some(pid), signal::SIGSTOP, true)?; + + ptrace::setregs(pid, regs_bak).unwrap(); + ptrace::syscall(pid, None).unwrap(); + Ok(()) +} + +// We only get dropped into these functions via offsetting the instr pointer +// manually, so we *must not ever* unwind from them. + +/// Disables protections on the page whose address is currently in `PAGE_ADDR`. +/// +/// SAFETY: `PAGE_ADDR` should be set to a page-aligned pointer to an owned page, +/// `PAGE_SIZE` should be the host pagesize, and the range from `PAGE_ADDR` to +/// `PAGE_SIZE` * `PAGE_COUNT` must be owned and allocated memory. No other threads +/// should be running. +pub unsafe extern "C" fn mempr_off() { + use std::sync::atomic::Ordering; + + // Again, cannot allow unwinds to happen here. + let len = PAGE_SIZE.load(Ordering::Relaxed).saturating_mul(PAGE_COUNT.load(Ordering::Relaxed)); + // SAFETY: Upheld by "caller". + unsafe { + // It's up to the caller to make sure this doesn't actually overflow, but + // we mustn't unwind from here, so... + if libc::mprotect( + PAGE_ADDR.load(Ordering::Relaxed).cast(), + len, + libc::PROT_READ | libc::PROT_WRITE, + ) != 0 + { + // Can't return or unwind, but we can do this. + std::process::exit(-1); + } + } + // If this fails somehow we're doomed. + if signal::raise(signal::SIGSTOP).is_err() { + std::process::exit(-1); + } +} + +/// Reenables protection on the page set by `PAGE_ADDR`. +/// +/// SAFETY: See `mempr_off()`. +pub unsafe extern "C" fn mempr_on() { + use std::sync::atomic::Ordering; + + let len = PAGE_SIZE.load(Ordering::Relaxed).wrapping_mul(PAGE_COUNT.load(Ordering::Relaxed)); + // SAFETY: Upheld by "caller". + unsafe { + if libc::mprotect(PAGE_ADDR.load(Ordering::Relaxed).cast(), len, libc::PROT_NONE) != 0 { + std::process::exit(-1); + } + } + if signal::raise(signal::SIGSTOP).is_err() { + std::process::exit(-1); + } +} diff --git a/src/tools/miri/src/shims/os_str.rs b/src/tools/miri/src/shims/os_str.rs index 7080edb26a5..b9391a0ffe0 100644 --- a/src/tools/miri/src/shims/os_str.rs +++ b/src/tools/miri/src/shims/os_str.rs @@ -7,7 +7,6 @@ use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::path::{Path, PathBuf}; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf; use crate::*; diff --git a/src/tools/miri/src/shims/unix/env.rs b/src/tools/miri/src/shims/unix/env.rs index 62ac7ee3806..604fb0974d2 100644 --- a/src/tools/miri/src/shims/unix/env.rs +++ b/src/tools/miri/src/shims/unix/env.rs @@ -6,7 +6,6 @@ use rustc_abi::{FieldIdx, Size}; use rustc_data_structures::fx::FxHashMap; use rustc_index::IndexVec; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf; use crate::*; diff --git a/src/tools/miri/src/shims/unix/foreign_items.rs b/src/tools/miri/src/shims/unix/foreign_items.rs index 9106ef94c43..b3c58397a02 100644 --- a/src/tools/miri/src/shims/unix/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/foreign_items.rs @@ -3,7 +3,6 @@ use std::str; use rustc_abi::{CanonAbi, ExternAbi, Size}; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf; use rustc_span::Symbol; use rustc_target::callconv::FnAbi; @@ -946,8 +945,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } "pthread_join" => { let [thread, retval] = this.check_shim(abi, CanonAbi::C, link_name, args)?; - let res = this.pthread_join(thread, retval)?; - this.write_scalar(res, dest)?; + this.pthread_join(thread, retval, dest)?; } "pthread_detach" => { let [thread] = this.check_shim(abi, CanonAbi::C, link_name, args)?; diff --git a/src/tools/miri/src/shims/unix/linux/mem.rs b/src/tools/miri/src/shims/unix/linux/mem.rs index 8e5a3021b1c..47732f811f1 100644 --- a/src/tools/miri/src/shims/unix/linux/mem.rs +++ b/src/tools/miri/src/shims/unix/linux/mem.rs @@ -22,8 +22,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let flags = this.read_scalar(flags)?.to_i32()?; // old_address must be a multiple of the page size - #[expect(clippy::arithmetic_side_effects)] // PAGE_SIZE is nonzero - if old_address.addr().bytes() % this.machine.page_size != 0 || new_size == 0 { + if !old_address.addr().bytes().is_multiple_of(this.machine.page_size) || new_size == 0 { this.set_last_error(LibcError("EINVAL"))?; return interp_ok(this.eval_libc("MAP_FAILED")); } diff --git a/src/tools/miri/src/shims/unix/mem.rs b/src/tools/miri/src/shims/unix/mem.rs index aefeee6f7a3..4bbbbc69c08 100644 --- a/src/tools/miri/src/shims/unix/mem.rs +++ b/src/tools/miri/src/shims/unix/mem.rs @@ -130,8 +130,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // addr must be a multiple of the page size, but apart from that munmap is just implemented // as a dealloc. - #[expect(clippy::arithmetic_side_effects)] // PAGE_SIZE is nonzero - if addr.addr().bytes() % this.machine.page_size != 0 { + if !addr.addr().bytes().is_multiple_of(this.machine.page_size) { return this.set_last_error_and_return_i32(LibcError("EINVAL")); } diff --git a/src/tools/miri/src/shims/unix/sync.rs b/src/tools/miri/src/shims/unix/sync.rs index eee2bbcb903..50eb4d92289 100644 --- a/src/tools/miri/src/shims/unix/sync.rs +++ b/src/tools/miri/src/shims/unix/sync.rs @@ -17,7 +17,7 @@ fn bytewise_equal_atomic_relaxed<'tcx>( // We do this in chunks of 4, so that we are okay to race with (sufficiently aligned) // 4-byte atomic accesses. - assert!(size.bytes() % 4 == 0); + assert!(size.bytes().is_multiple_of(4)); for i in 0..(size.bytes() / 4) { let offset = Size::from_bytes(i.strict_mul(4)); let load = |place: &MPlaceTy<'tcx>| { diff --git a/src/tools/miri/src/shims/unix/thread.rs b/src/tools/miri/src/shims/unix/thread.rs index 4b6615b3ea8..a438e71a41d 100644 --- a/src/tools/miri/src/shims/unix/thread.rs +++ b/src/tools/miri/src/shims/unix/thread.rs @@ -41,7 +41,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { &mut self, thread: &OpTy<'tcx>, retval: &OpTy<'tcx>, - ) -> InterpResult<'tcx, Scalar> { + return_dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { let this = self.eval_context_mut(); if !this.ptr_is_null(this.read_pointer(retval)?)? { @@ -51,12 +52,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let thread = this.read_scalar(thread)?.to_int(this.libc_ty_layout("pthread_t").size)?; let Ok(thread) = this.thread_id_try_from(thread) else { - return interp_ok(this.eval_libc("ESRCH")); + this.write_scalar(this.eval_libc("ESRCH"), return_dest)?; + return interp_ok(()); }; - this.join_thread_exclusive(thread)?; - - interp_ok(Scalar::from_u32(0)) + this.join_thread_exclusive( + thread, + /* success_retval */ Scalar::from_u32(0), + return_dest, + ) } fn pthread_detach(&mut self, thread: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { diff --git a/src/tools/miri/src/shims/windows/foreign_items.rs b/src/tools/miri/src/shims/windows/foreign_items.rs index 10f6df67ad4..de10357f5fa 100644 --- a/src/tools/miri/src/shims/windows/foreign_items.rs +++ b/src/tools/miri/src/shims/windows/foreign_items.rs @@ -573,8 +573,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { "WaitForSingleObject" => { let [handle, timeout] = this.check_shim(abi, sys_conv, link_name, args)?; - let ret = this.WaitForSingleObject(handle, timeout)?; - this.write_scalar(ret, dest)?; + this.WaitForSingleObject(handle, timeout, dest)?; } "GetCurrentProcess" => { let [] = this.check_shim(abi, sys_conv, link_name, args)?; diff --git a/src/tools/miri/src/shims/windows/thread.rs b/src/tools/miri/src/shims/windows/thread.rs index d5f9ed4e968..a155ec65a51 100644 --- a/src/tools/miri/src/shims/windows/thread.rs +++ b/src/tools/miri/src/shims/windows/thread.rs @@ -1,5 +1,4 @@ use rustc_abi::ExternAbi; -use rustc_middle::ty::layout::LayoutOf; use self::shims::windows::handle::{EvalContextExt as _, Handle, PseudoHandle}; use crate::*; @@ -59,13 +58,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { &mut self, handle_op: &OpTy<'tcx>, timeout_op: &OpTy<'tcx>, - ) -> InterpResult<'tcx, Scalar> { + return_dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { let this = self.eval_context_mut(); let handle = this.read_handle(handle_op, "WaitForSingleObject")?; let timeout = this.read_scalar(timeout_op)?.to_u32()?; - let thread = match handle { + let joined_thread_id = match handle { Handle::Thread(thread) => thread, // Unlike on posix, the outcome of joining the current thread is not documented. // On current Windows, it just deadlocks. @@ -77,8 +77,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { throw_unsup_format!("`WaitForSingleObject` with non-infinite timeout"); } - this.join_thread(thread)?; + this.join_thread( + joined_thread_id, + /* success_retval */ this.eval_windows("c", "WAIT_OBJECT_0"), + return_dest, + )?; - interp_ok(this.eval_windows("c", "WAIT_OBJECT_0")) + interp_ok(()) } } diff --git a/src/tools/miri/src/shims/x86/aesni.rs b/src/tools/miri/src/shims/x86/aesni.rs index 7191284b5a3..058ca24e730 100644 --- a/src/tools/miri/src/shims/x86/aesni.rs +++ b/src/tools/miri/src/shims/x86/aesni.rs @@ -1,6 +1,5 @@ use rustc_abi::CanonAbi; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf as _; use rustc_span::Symbol; use rustc_target::callconv::FnAbi; diff --git a/src/tools/miri/src/shims/x86/avx.rs b/src/tools/miri/src/shims/x86/avx.rs index 37539fec748..83d23d6ad36 100644 --- a/src/tools/miri/src/shims/x86/avx.rs +++ b/src/tools/miri/src/shims/x86/avx.rs @@ -2,7 +2,6 @@ use rustc_abi::CanonAbi; use rustc_apfloat::ieee::{Double, Single}; use rustc_middle::mir; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf as _; use rustc_span::Symbol; use rustc_target::callconv::FnAbi; diff --git a/src/tools/miri/src/shims/x86/avx2.rs b/src/tools/miri/src/shims/x86/avx2.rs index 5dfe5cc2c54..49d5977078b 100644 --- a/src/tools/miri/src/shims/x86/avx2.rs +++ b/src/tools/miri/src/shims/x86/avx2.rs @@ -1,7 +1,6 @@ use rustc_abi::CanonAbi; use rustc_middle::mir; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf as _; use rustc_span::Symbol; use rustc_target::callconv::FnAbi; diff --git a/src/tools/miri/src/shims/x86/mod.rs b/src/tools/miri/src/shims/x86/mod.rs index 1e82f521249..fbfe459711e 100644 --- a/src/tools/miri/src/shims/x86/mod.rs +++ b/src/tools/miri/src/shims/x86/mod.rs @@ -2,7 +2,6 @@ use rustc_abi::{CanonAbi, FieldIdx, Size}; use rustc_apfloat::Float; use rustc_apfloat::ieee::Single; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf as _; use rustc_middle::{mir, ty}; use rustc_span::Symbol; use rustc_target::callconv::FnAbi; diff --git a/src/tools/miri/src/shims/x86/sse42.rs b/src/tools/miri/src/shims/x86/sse42.rs index 830513f0291..7e1e1482ef4 100644 --- a/src/tools/miri/src/shims/x86/sse42.rs +++ b/src/tools/miri/src/shims/x86/sse42.rs @@ -1,7 +1,6 @@ use rustc_abi::{CanonAbi, Size}; use rustc_middle::mir; use rustc_middle::ty::Ty; -use rustc_middle::ty::layout::LayoutOf as _; use rustc_span::Symbol; use rustc_target::callconv::FnAbi; diff --git a/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs b/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs index c63e926376d..9646cdb2065 100644 --- a/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs +++ b/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs @@ -6,7 +6,7 @@ fn direct_raw(x: *const (i32, i32)) -> *const i32 { // Ensure that if a raw pointer is created via an intermediate // reference, we catch that. (Just in case someone decides to -// desugar this differenly or so.) +// desugar this differently or so.) fn via_ref(x: *const (i32, i32)) -> *const i32 { unsafe { &(*x).0 as *const i32 } //~ERROR: dangling pointer } diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 383579198bb..6b782f55359 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1358,7 +1358,7 @@ fn test_min_max_nondet() { /// Ensure that if we call the closure often enough, we see both `true` and `false.` #[track_caller] fn ensure_both(f: impl Fn() -> bool) { - let rounds = 16; + let rounds = 32; let first = f(); for _ in 1..rounds { if f() != first { diff --git a/src/tools/miri/tests/pass/intrinsics/portable-simd.rs b/src/tools/miri/tests/pass/intrinsics/portable-simd.rs index e14ce51f35a..726d4c01cc3 100644 --- a/src/tools/miri/tests/pass/intrinsics/portable-simd.rs +++ b/src/tools/miri/tests/pass/intrinsics/portable-simd.rs @@ -570,6 +570,10 @@ fn simd_round() { f32x4::from_array([1.0, 1.0, 2.0, -5.0]) ); assert_eq!( + unsafe { intrinsics::simd_round_ties_even(f32x4::from_array([0.9, 1.001, 2.0, -4.5])) }, + f32x4::from_array([1.0, 1.0, 2.0, -4.0]) + ); + assert_eq!( f32x4::from_array([0.9, 1.001, 2.0, -4.5]).trunc(), f32x4::from_array([0.0, 1.0, 2.0, -4.0]) ); @@ -587,6 +591,10 @@ fn simd_round() { f64x4::from_array([1.0, 1.0, 2.0, -5.0]) ); assert_eq!( + unsafe { intrinsics::simd_round_ties_even(f64x4::from_array([0.9, 1.001, 2.0, -4.5])) }, + f64x4::from_array([1.0, 1.0, 2.0, -4.0]) + ); + assert_eq!( f64x4::from_array([0.9, 1.001, 2.0, -4.5]).trunc(), f64x4::from_array([0.0, 1.0, 2.0, -4.0]) ); diff --git a/src/tools/opt-dist/src/exec.rs b/src/tools/opt-dist/src/exec.rs index 64ce5cc3775..0dc6e56b9d5 100644 --- a/src/tools/opt-dist/src/exec.rs +++ b/src/tools/opt-dist/src/exec.rs @@ -122,7 +122,12 @@ impl Bootstrap { let metrics_path = env.build_root().join("build").join("metrics.json"); let args = dist_args.iter().map(|arg| arg.as_str()).collect::<Vec<_>>(); let cmd = cmd(&args).env("RUST_BACKTRACE", "full"); - let cmd = add_shared_x_flags(env, cmd); + let mut cmd = add_shared_x_flags(env, cmd); + if env.is_fast_try_build() { + // We set build.extended=false for fast try builds, but we still need Cargo + cmd = cmd.arg("cargo"); + } + Self { cmd, metrics_path } } @@ -189,5 +194,18 @@ impl Bootstrap { } fn add_shared_x_flags(env: &Environment, cmd: CmdBuilder) -> CmdBuilder { - if env.is_fast_try_build() { cmd.arg("--set").arg("rust.deny-warnings=false") } else { cmd } + if env.is_fast_try_build() { + // Skip things that cannot be skipped through `x ... --skip` + cmd.arg("--set") + .arg("rust.llvm-bitcode-linker=false") + // Skip wasm-component-ld. This also skips cargo, which we need to re-enable for dist + .arg("--set") + .arg("build.extended=false") + .arg("--set") + .arg("rust.codegen-backends=['llvm']") + .arg("--set") + .arg("rust.deny-warnings=false") + } else { + cmd + } } diff --git a/src/tools/opt-dist/src/main.rs b/src/tools/opt-dist/src/main.rs index d2827ec01ca..9c8a6637a3b 100644 --- a/src/tools/opt-dist/src/main.rs +++ b/src/tools/opt-dist/src/main.rs @@ -407,13 +407,18 @@ fn main() -> anyhow::Result<()> { for target in [ "rust-docs", "rustc-docs", + "rustc-dev", + "rust-dev", "rust-docs-json", "rust-analyzer", "rustc-src", + "extended", "clippy", "miri", "rustfmt", "gcc", + "generate-copyright", + "bootstrap", ] { build_args.extend(["--skip".to_string(), target.to_string()]); } diff --git a/src/tools/run-make-support/src/lib.rs b/src/tools/run-make-support/src/lib.rs index 947f815fd69..67d8c351a59 100644 --- a/src/tools/run-make-support/src/lib.rs +++ b/src/tools/run-make-support/src/lib.rs @@ -83,7 +83,7 @@ pub use run::{cmd, run, run_fail, run_with_args}; /// Helpers for checking target information. pub use targets::{ - apple_os, is_aix, is_darwin, is_msvc, is_windows, is_windows_gnu, is_win7, llvm_components_contain, + apple_os, is_aix, is_darwin, is_msvc, is_windows, is_windows_gnu, is_windows_msvc, is_win7, llvm_components_contain, target, uname, }; diff --git a/src/tools/run-make-support/src/targets.rs b/src/tools/run-make-support/src/targets.rs index 86edbdf750b..1ab2e2ab2be 100644 --- a/src/tools/run-make-support/src/targets.rs +++ b/src/tools/run-make-support/src/targets.rs @@ -28,6 +28,12 @@ pub fn is_windows_gnu() -> bool { target().ends_with("windows-gnu") } +/// Check if target is windows-msvc. +#[must_use] +pub fn is_windows_msvc() -> bool { + target().ends_with("windows-msvc") +} + /// Check if target is win7. #[must_use] pub fn is_win7() -> bool { diff --git a/src/tools/rust-analyzer/rust-version b/src/tools/rust-analyzer/rust-version index 786c6568020..902793225ea 100644 --- a/src/tools/rust-analyzer/rust-version +++ b/src/tools/rust-analyzer/rust-version @@ -1 +1 @@ -58d5e1169056f31553ecf680b009a5770eb0e859 +ad3b7257615c28aaf8212a189ec032b8af75de51 diff --git a/src/tools/rust-installer/install-template.sh b/src/tools/rust-installer/install-template.sh index f7f408be882..337aaa95b9a 100644 --- a/src/tools/rust-installer/install-template.sh +++ b/src/tools/rust-installer/install-template.sh @@ -160,7 +160,7 @@ valopt() { local doc="$*" if [ $HELP -eq 0 ] then - local uop=$(echo $op | tr 'a-z-' 'A-Z_') + local uop=$(echo $op | tr '[a-z]-' '[A-Z]_') local v="CFG_${uop}" eval $v="$default" for arg in $CFG_ARGS @@ -206,8 +206,8 @@ opt() { do if [ "$arg" = "--${flag}-${op}" ] then - op=$(echo $op | tr 'a-z-' 'A-Z_') - flag=$(echo $flag | tr 'a-z' 'A-Z') + op=$(echo $op | tr '[a-z]-' '[A-Z]_') + flag=$(echo $flag | tr '[a-z]' '[A-Z]') local v="CFG_${flag}_${op}" eval $v=1 putvar $v @@ -235,7 +235,7 @@ flag() { do if [ "$arg" = "--${op}" ] then - op=$(echo $op | tr 'a-z-' 'A-Z_') + op=$(echo $op | tr '[a-z]-' '[A-Z]_') local v="CFG_${op}" eval $v=1 putvar $v diff --git a/src/tools/rustfmt/src/expr.rs b/src/tools/rustfmt/src/expr.rs index be6b483bfff..08aedff2b20 100644 --- a/src/tools/rustfmt/src/expr.rs +++ b/src/tools/rustfmt/src/expr.rs @@ -2289,8 +2289,10 @@ fn rewrite_expr_addrof( ) -> RewriteResult { let operator_str = match (mutability, borrow_kind) { (ast::Mutability::Not, ast::BorrowKind::Ref) => "&", + (ast::Mutability::Not, ast::BorrowKind::Pin) => "&pin const ", (ast::Mutability::Not, ast::BorrowKind::Raw) => "&raw const ", (ast::Mutability::Mut, ast::BorrowKind::Ref) => "&mut ", + (ast::Mutability::Mut, ast::BorrowKind::Pin) => "&pin mut ", (ast::Mutability::Mut, ast::BorrowKind::Raw) => "&raw mut ", }; rewrite_unary_prefix(context, operator_str, expr, shape) diff --git a/src/tools/rustfmt/tests/source/pin_sugar.rs b/src/tools/rustfmt/tests/source/pin_sugar.rs index 370dfbc196a..e5b47339b92 100644 --- a/src/tools/rustfmt/tests/source/pin_sugar.rs +++ b/src/tools/rustfmt/tests/source/pin_sugar.rs @@ -18,3 +18,13 @@ impl Foo { mut self) {} fn i(&pin mut self) {} } + +fn borrows() { + let mut foo = 0_i32; + let x: Pin<&mut _> = & pin + mut foo; + + let x: Pin<&_> = & + pin const + foo; +} diff --git a/src/tools/rustfmt/tests/source/type.rs b/src/tools/rustfmt/tests/source/type.rs index 7a232f85198..213fad7cb16 100644 --- a/src/tools/rustfmt/tests/source/type.rs +++ b/src/tools/rustfmt/tests/source/type.rs @@ -142,18 +142,18 @@ type MyFn = fn(a: SomeLongComplexType, b: SomeOtherLongComplexType,) -> Box<Futu // Const bound -trait T: ~ const Super {} +trait T: [ const ] Super {} -const fn not_quite_const<S: ~ const T>() -> i32 { <S as T>::CONST } +const fn not_quite_const<S: [ const ] T>() -> i32 { <S as T>::CONST } -impl ~ const T {} +impl const T for U {} -fn apit(_: impl ~ const T) {} +fn apit(_: impl [ const ] T) {} -fn rpit() -> impl ~ const T { S } +fn rpit() -> impl [ const] T { S } pub struct Foo<T: Trait>(T); -impl<T: ~ const Trait> Foo<T> { +impl<T: [ const] Trait> Foo<T> { fn new(t: T) -> Self { Self(t) } diff --git a/src/tools/rustfmt/tests/target/pin_sugar.rs b/src/tools/rustfmt/tests/target/pin_sugar.rs index 7d04efb1b32..09ad23a5807 100644 --- a/src/tools/rustfmt/tests/target/pin_sugar.rs +++ b/src/tools/rustfmt/tests/target/pin_sugar.rs @@ -16,3 +16,10 @@ impl Foo { fn h<'a>(&'a pin mut self) {} fn i(&pin mut self) {} } + +fn borrows() { + let mut foo = 0_i32; + let x: Pin<&mut _> = &pin mut foo; + + let x: Pin<&_> = &pin const foo; +} diff --git a/src/tools/rustfmt/tests/target/type.rs b/src/tools/rustfmt/tests/target/type.rs index 325adb52f3f..93479f8b484 100644 --- a/src/tools/rustfmt/tests/target/type.rs +++ b/src/tools/rustfmt/tests/target/type.rs @@ -147,22 +147,22 @@ type MyFn = fn( // Const bound -trait T: ~const Super {} +trait T: [const] Super {} -const fn not_quite_const<S: ~const T>() -> i32 { +const fn not_quite_const<S: [const] T>() -> i32 { <S as T>::CONST } -impl ~const T {} +impl const T for U {} -fn apit(_: impl ~const T) {} +fn apit(_: impl [const] T) {} -fn rpit() -> impl ~const T { +fn rpit() -> impl [const] T { S } pub struct Foo<T: Trait>(T); -impl<T: ~const Trait> Foo<T> { +impl<T: [const] Trait> Foo<T> { fn new(t: T) -> Self { Self(t) } diff --git a/src/tools/test-float-parse/src/ui.rs b/src/tools/test-float-parse/src/ui.rs index 73473eef0bf..1a9ba0dc1d9 100644 --- a/src/tools/test-float-parse/src/ui.rs +++ b/src/tools/test-float-parse/src/ui.rs @@ -118,7 +118,7 @@ pub fn finish_all(tests: &[TestInfo], total_elapsed: Duration, cfg: &Config) -> match result { Ok(FinishedAll) => (), Err(EarlyExit::Timeout) => { - println!(" exited early; exceded {:?} timeout", cfg.timeout) + println!(" exited early; exceeded {:?} timeout", cfg.timeout) } Err(EarlyExit::MaxFailures) => { println!(" exited early; exceeded {:?} max failures", cfg.max_failures) diff --git a/src/tools/tidy/src/alphabetical.rs b/src/tools/tidy/src/alphabetical.rs index a29286fa2c5..141083290c6 100644 --- a/src/tools/tidy/src/alphabetical.rs +++ b/src/tools/tidy/src/alphabetical.rs @@ -19,7 +19,9 @@ //! If a line ends with an opening delimiter, we effectively join the following line to it before //! checking it. E.g. `foo(\nbar)` is treated like `foo(bar)`. +use std::cmp::Ordering; use std::fmt::Display; +use std::iter::Peekable; use std::path::Path; use crate::walk::{filter_dirs, walk}; @@ -99,9 +101,9 @@ fn check_section<'a>( continue; } - let prev_line_trimmed_lowercase = prev_line.trim_start_matches(' ').to_lowercase(); + let prev_line_trimmed_lowercase = prev_line.trim_start_matches(' '); - if trimmed_line.to_lowercase() < prev_line_trimmed_lowercase { + if version_sort(&trimmed_line, &prev_line_trimmed_lowercase).is_lt() { tidy_error_ext!(err, bad, "{file}:{}: line not in alphabetical order", idx + 1); } @@ -143,3 +145,56 @@ pub fn check(path: &Path, bad: &mut bool) { check_lines(file, lines, &mut crate::tidy_error, bad) }); } + +fn consume_numeric_prefix<I: Iterator<Item = char>>(it: &mut Peekable<I>) -> String { + let mut result = String::new(); + + while let Some(&c) = it.peek() { + if !c.is_numeric() { + break; + } + + result.push(c); + it.next(); + } + + result +} + +// A sorting function that is case-sensitive, and sorts sequences of digits by their numeric value, +// so that `9` sorts before `12`. +fn version_sort(a: &str, b: &str) -> Ordering { + let mut it1 = a.chars().peekable(); + let mut it2 = b.chars().peekable(); + + while let (Some(x), Some(y)) = (it1.peek(), it2.peek()) { + match (x.is_numeric(), y.is_numeric()) { + (true, true) => { + let num1: String = consume_numeric_prefix(it1.by_ref()); + let num2: String = consume_numeric_prefix(it2.by_ref()); + + let int1: u64 = num1.parse().unwrap(); + let int2: u64 = num2.parse().unwrap(); + + // Compare strings when the numeric value is equal to handle "00" versus "0". + match int1.cmp(&int2).then_with(|| num1.cmp(&num2)) { + Ordering::Equal => continue, + different => return different, + } + } + (false, false) => match x.cmp(y) { + Ordering::Equal => { + it1.next(); + it2.next(); + continue; + } + different => return different, + }, + (false, true) | (true, false) => { + return x.cmp(y); + } + } + } + + it1.next().cmp(&it2.next()) +} diff --git a/src/tools/tidy/src/alphabetical/tests.rs b/src/tools/tidy/src/alphabetical/tests.rs index 29e89a693bf..4d05bc33ced 100644 --- a/src/tools/tidy/src/alphabetical/tests.rs +++ b/src/tools/tidy/src/alphabetical/tests.rs @@ -3,6 +3,7 @@ use std::str::from_utf8; use super::*; +#[track_caller] fn test(lines: &str, name: &str, expected_msg: &str, expected_bad: bool) { let mut actual_msg = Vec::new(); let mut actual_bad = false; @@ -15,10 +16,12 @@ fn test(lines: &str, name: &str, expected_msg: &str, expected_bad: bool) { assert_eq!(expected_bad, actual_bad); } +#[track_caller] fn good(lines: &str) { test(lines, "good", "", false); } +#[track_caller] fn bad(lines: &str, expected_msg: &str) { test(lines, "bad", expected_msg, true); } @@ -187,3 +190,147 @@ fn test_double_end() { "; bad(lines, "bad:5 found `tidy-alphabetical-end` expecting `tidy-alphabetical-start`"); } + +#[test] +fn test_numeric_good() { + good( + "\ + # tidy-alphabetical-start + rustc_ast = { path = \"../rustc_ast\" } + rustc_ast_lowering = { path = \"../rustc_ast_lowering\" } + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + fp-armv8 + fp16 + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + item1 + item2 + item10 + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + foo + foo_ + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + foo-bar + foo_bar + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + sme-lutv2 + sme2 + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + v5te + v6 + v6k + v6t2 + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + zve64d + zve64f + # tidy-alphabetical-end + ", + ); + + // Case is significant. + good( + "\ + # tidy-alphabetical-start + _ZYXW + _abcd + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + v0 + v00 + v000 + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + w005s09t + w5s009t + # tidy-alphabetical-end + ", + ); + + good( + "\ + # tidy-alphabetical-start + v0s + v00t + # tidy-alphabetical-end + ", + ); +} + +#[test] +fn test_numeric_bad() { + let lines = "\ + # tidy-alphabetical-start + item1 + item10 + item2 + # tidy-alphabetical-end + "; + bad(lines, "bad:4: line not in alphabetical order"); + + let lines = "\ + # tidy-alphabetical-start + zve64f + zve64d + # tidy-alphabetical-end + "; + bad(lines, "bad:3: line not in alphabetical order"); + + let lines = "\ + # tidy-alphabetical-start + 000 + 00 + # tidy-alphabetical-end + "; + bad(lines, "bad:3: line not in alphabetical order"); +} diff --git a/src/tools/tidy/src/deps.rs b/src/tools/tidy/src/deps.rs index 716d42c32eb..bf813d2131e 100644 --- a/src/tools/tidy/src/deps.rs +++ b/src/tools/tidy/src/deps.rs @@ -206,6 +206,7 @@ const EXCEPTIONS_CRANELIFT: ExceptionList = &[ ("regalloc2", "Apache-2.0 WITH LLVM-exception"), ("target-lexicon", "Apache-2.0 WITH LLVM-exception"), ("wasmtime-jit-icache-coherence", "Apache-2.0 WITH LLVM-exception"), + ("wasmtime-math", "Apache-2.0 WITH LLVM-exception"), // tidy-alphabetical-end ]; @@ -373,6 +374,7 @@ const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[ "scoped-tls", "scopeguard", "self_cell", + "semver", "serde", "serde_derive", "serde_json", @@ -537,6 +539,7 @@ const PERMITTED_CRANELIFT_DEPENDENCIES: &[&str] = &[ "indexmap", "libc", "libloading", + "libm", "log", "mach2", "memchr", @@ -554,6 +557,7 @@ const PERMITTED_CRANELIFT_DEPENDENCIES: &[&str] = &[ "target-lexicon", "unicode-ident", "wasmtime-jit-icache-coherence", + "wasmtime-math", "windows-sys", "windows-targets", "windows_aarch64_gnullvm", diff --git a/src/tools/tidy/src/error_codes.rs b/src/tools/tidy/src/error_codes.rs index e2d1b85797f..bb61412f678 100644 --- a/src/tools/tidy/src/error_codes.rs +++ b/src/tools/tidy/src/error_codes.rs @@ -43,9 +43,18 @@ macro_rules! verbose_print { }; } -pub fn check(root_path: &Path, search_paths: &[&Path], verbose: bool, bad: &mut bool) { +pub fn check( + root_path: &Path, + search_paths: &[&Path], + verbose: bool, + ci_info: &crate::CiInfo, + bad: &mut bool, +) { let mut errors = Vec::new(); + // Check that no error code explanation was removed. + check_removed_error_code_explanation(ci_info, bad); + // Stage 1: create list let error_codes = extract_error_codes(root_path, &mut errors); if verbose { @@ -68,6 +77,27 @@ pub fn check(root_path: &Path, search_paths: &[&Path], verbose: bool, bad: &mut } } +fn check_removed_error_code_explanation(ci_info: &crate::CiInfo, bad: &mut bool) { + let Some(base_commit) = &ci_info.base_commit else { + eprintln!("Skipping error code explanation removal check"); + return; + }; + let Some(diff) = crate::git_diff(base_commit, "--name-status") else { + *bad = true; + eprintln!("removed error code explanation tidy check: Failed to run git diff"); + return; + }; + if diff.lines().any(|line| { + line.starts_with('D') && line.contains("compiler/rustc_error_codes/src/error_codes/") + }) { + *bad = true; + eprintln!("tidy check error: Error code explanations should never be removed!"); + eprintln!("Take a look at E0001 to see how to handle it."); + return; + } + println!("No error code explanation was removed!"); +} + /// Stage 1: Parses a list of error codes from `error_codes.rs`. fn extract_error_codes(root_path: &Path, errors: &mut Vec<String>) -> Vec<String> { let path = root_path.join(Path::new(ERROR_CODES_PATH)); diff --git a/src/tools/tidy/src/issues.txt b/src/tools/tidy/src/issues.txt index b3517b2e9da..7e27e0258c2 100644 --- a/src/tools/tidy/src/issues.txt +++ b/src/tools/tidy/src/issues.txt @@ -2070,7 +2070,6 @@ ui/issues/issue-32782.rs ui/issues/issue-32797.rs ui/issues/issue-32805.rs ui/issues/issue-3290.rs -ui/issues/issue-32950.rs ui/issues/issue-32995-2.rs ui/issues/issue-32995.rs ui/issues/issue-33202.rs @@ -2340,7 +2339,6 @@ ui/issues/issue-49934.rs ui/issues/issue-49955.rs ui/issues/issue-49973.rs ui/issues/issue-50187.rs -ui/issues/issue-50403.rs ui/issues/issue-50411.rs ui/issues/issue-50415.rs ui/issues/issue-50442.rs @@ -3921,7 +3919,6 @@ ui/traits/const-traits/issue-79450.rs ui/traits/const-traits/issue-88155.rs ui/traits/const-traits/issue-92111.rs ui/traits/const-traits/issue-92230-wf-super-trait-env.rs -ui/traits/const-traits/specialization/issue-95186-specialize-on-tilde-const.rs ui/traits/const-traits/specialization/issue-95187-same-trait-bound-different-constness.rs ui/traits/issue-103563.rs ui/traits/issue-104322.rs diff --git a/src/tools/tidy/src/lib.rs b/src/tools/tidy/src/lib.rs index 28aa80225b1..237737f0f16 100644 --- a/src/tools/tidy/src/lib.rs +++ b/src/tools/tidy/src/lib.rs @@ -3,6 +3,12 @@ //! This library contains the tidy lints and exposes it //! to be used by tools. +use std::ffi::OsStr; +use std::process::Command; + +use build_helper::ci::CiEnv; +use build_helper::git::{GitConfig, get_closest_upstream_commit}; +use build_helper::stage0_parser::{Stage0Config, parse_stage0_file}; use termcolor::WriteColor; macro_rules! static_regex { @@ -63,6 +69,61 @@ fn tidy_error(args: &str) -> std::io::Result<()> { Ok(()) } +pub struct CiInfo { + pub git_merge_commit_email: String, + pub nightly_branch: String, + pub base_commit: Option<String>, + pub ci_env: CiEnv, +} + +impl CiInfo { + pub fn new(bad: &mut bool) -> Self { + let stage0 = parse_stage0_file(); + let Stage0Config { nightly_branch, git_merge_commit_email, .. } = stage0.config; + + let mut info = Self { + nightly_branch, + git_merge_commit_email, + ci_env: CiEnv::current(), + base_commit: None, + }; + let base_commit = match get_closest_upstream_commit(None, &info.git_config(), info.ci_env) { + Ok(Some(commit)) => Some(commit), + Ok(None) => { + info.error_if_in_ci("no base commit found", bad); + None + } + Err(error) => { + info.error_if_in_ci(&format!("failed to retrieve base commit: {error}"), bad); + None + } + }; + info.base_commit = base_commit; + info + } + + pub fn git_config(&self) -> GitConfig<'_> { + GitConfig { + nightly_branch: &self.nightly_branch, + git_merge_commit_email: &self.git_merge_commit_email, + } + } + + pub fn error_if_in_ci(&self, msg: &str, bad: &mut bool) { + if self.ci_env.is_running_in_ci() { + *bad = true; + eprintln!("tidy check error: {msg}"); + } else { + eprintln!("tidy check warning: {msg}. Some checks will be skipped."); + } + } +} + +pub fn git_diff<S: AsRef<OsStr>>(base_commit: &str, extra_arg: S) -> Option<String> { + let output = Command::new("git").arg("diff").arg(base_commit).arg(extra_arg).output().ok()?; + Some(String::from_utf8_lossy(&output.stdout).into()) +} + pub mod alphabetical; pub mod bins; pub mod debug_artifacts; diff --git a/src/tools/tidy/src/main.rs b/src/tools/tidy/src/main.rs index 0b66017b865..ef6ff5c9277 100644 --- a/src/tools/tidy/src/main.rs +++ b/src/tools/tidy/src/main.rs @@ -48,7 +48,9 @@ fn main() { let extra_checks = cfg_args.iter().find(|s| s.starts_with("--extra-checks=")).map(String::as_str); - let bad = std::sync::Arc::new(AtomicBool::new(false)); + let mut bad = false; + let ci_info = CiInfo::new(&mut bad); + let bad = std::sync::Arc::new(AtomicBool::new(bad)); let drain_handles = |handles: &mut VecDeque<ScopedJoinHandle<'_, ()>>| { // poll all threads for completion before awaiting the oldest one @@ -110,12 +112,12 @@ fn main() { check!(rustdoc_css_themes, &librustdoc_path); check!(rustdoc_templates, &librustdoc_path); check!(rustdoc_js, &librustdoc_path, &tools_path, &src_path); - check!(rustdoc_json, &src_path); + check!(rustdoc_json, &src_path, &ci_info); check!(known_bug, &crashes_path); check!(unknown_revision, &tests_path); // Checks that only make sense for the compiler. - check!(error_codes, &root_path, &[&compiler_path, &librustdoc_path], verbose); + check!(error_codes, &root_path, &[&compiler_path, &librustdoc_path], verbose, &ci_info); check!(fluent_alphabetical, &compiler_path, bless); check!(fluent_period, &compiler_path); check!(target_policy, &root_path); diff --git a/src/tools/tidy/src/rustdoc_json.rs b/src/tools/tidy/src/rustdoc_json.rs index 2377356e14d..dfbb35d69f1 100644 --- a/src/tools/tidy/src/rustdoc_json.rs +++ b/src/tools/tidy/src/rustdoc_json.rs @@ -1,56 +1,20 @@ //! Tidy check to ensure that `FORMAT_VERSION` was correctly updated if `rustdoc-json-types` was //! updated as well. -use std::ffi::OsStr; use std::path::Path; -use std::process::Command; use std::str::FromStr; -use build_helper::ci::CiEnv; -use build_helper::git::{GitConfig, get_closest_upstream_commit}; -use build_helper::stage0_parser::parse_stage0_file; - const RUSTDOC_JSON_TYPES: &str = "src/rustdoc-json-types"; -fn git_diff<S: AsRef<OsStr>>(base_commit: &str, extra_arg: S) -> Option<String> { - let output = Command::new("git").arg("diff").arg(base_commit).arg(extra_arg).output().ok()?; - Some(String::from_utf8_lossy(&output.stdout).into()) -} - -fn error_if_in_ci(ci_env: CiEnv, msg: &str, bad: &mut bool) { - if ci_env.is_running_in_ci() { - *bad = true; - eprintln!("error in `rustdoc_json` tidy check: {msg}"); - } else { - eprintln!("{msg}. Skipping `rustdoc_json` tidy check"); - } -} - -pub fn check(src_path: &Path, bad: &mut bool) { +pub fn check(src_path: &Path, ci_info: &crate::CiInfo, bad: &mut bool) { println!("Checking tidy rustdoc_json..."); - let stage0 = parse_stage0_file(); - let ci_env = CiEnv::current(); - let base_commit = match get_closest_upstream_commit( - None, - &GitConfig { - nightly_branch: &stage0.config.nightly_branch, - git_merge_commit_email: &stage0.config.git_merge_commit_email, - }, - ci_env, - ) { - Ok(Some(commit)) => commit, - Ok(None) => { - error_if_in_ci(ci_env, "no base commit found", bad); - return; - } - Err(error) => { - error_if_in_ci(ci_env, &format!("failed to retrieve base commit: {error}"), bad); - return; - } + let Some(base_commit) = &ci_info.base_commit else { + eprintln!("No base commit, skipping rustdoc_json check"); + return; }; // First we check that `src/rustdoc-json-types` was modified. - match git_diff(&base_commit, "--name-status") { + match crate::git_diff(&base_commit, "--name-status") { Some(output) => { if !output .lines() @@ -68,7 +32,7 @@ pub fn check(src_path: &Path, bad: &mut bool) { } } // Then we check that if `FORMAT_VERSION` was updated, the `Latest feature:` was also updated. - match git_diff(&base_commit, src_path.join("rustdoc-json-types")) { + match crate::git_diff(&base_commit, src_path.join("rustdoc-json-types")) { Some(output) => { let mut format_version_updated = false; let mut latest_feature_comment_updated = false; diff --git a/src/tools/tidy/src/ui_tests.rs b/src/tools/tidy/src/ui_tests.rs index 8f9b07c49ac..53226fcb80e 100644 --- a/src/tools/tidy/src/ui_tests.rs +++ b/src/tools/tidy/src/ui_tests.rs @@ -17,7 +17,7 @@ use ignore::Walk; const ENTRY_LIMIT: u32 = 901; // FIXME: The following limits should be reduced eventually. -const ISSUES_ENTRY_LIMIT: u32 = 1623; +const ISSUES_ENTRY_LIMIT: u32 = 1619; const EXPECTED_TEST_FILE_EXTENSIONS: &[&str] = &[ "rs", // test source files diff --git a/src/tools/wasm-component-ld/Cargo.toml b/src/tools/wasm-component-ld/Cargo.toml index 642d48b9952..ce718902b29 100644 --- a/src/tools/wasm-component-ld/Cargo.toml +++ b/src/tools/wasm-component-ld/Cargo.toml @@ -10,4 +10,4 @@ name = "wasm-component-ld" path = "src/main.rs" [dependencies] -wasm-component-ld = "0.5.13" +wasm-component-ld = "0.5.14" |
