diff --git a/CHANGELOG.md b/CHANGELOG.md index ce5b8fb392ff..2757597fc511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6066,6 +6066,7 @@ Released 2018-09-13 [`size_of_in_element_count`]: https://rust-lang.github.io/rust-clippy/master/index.html#size_of_in_element_count [`size_of_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#size_of_ref [`skip_while_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#skip_while_next +[`sliced_string_as_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#sliced_string_as_bytes [`slow_vector_initialization`]: https://rust-lang.github.io/rust-clippy/master/index.html#slow_vector_initialization [`stable_sort_primitive`]: https://rust-lang.github.io/rust-clippy/master/index.html#stable_sort_primitive [`std_instead_of_alloc`]: https://rust-lang.github.io/rust-clippy/master/index.html#std_instead_of_alloc diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 86410c16d95f..ec223381aec6 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -468,6 +468,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::methods::SHOULD_IMPLEMENT_TRAIT_INFO, crate::methods::SINGLE_CHAR_ADD_STR_INFO, crate::methods::SKIP_WHILE_NEXT_INFO, + crate::methods::SLICED_STRING_AS_BYTES_INFO, crate::methods::STABLE_SORT_PRIMITIVE_INFO, crate::methods::STRING_EXTEND_CHARS_INFO, crate::methods::STRING_LIT_CHARS_ANY_INFO, diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs index 9bfa59479905..ceb21f627c97 100644 --- a/clippy_lints/src/methods/mod.rs +++ b/clippy_lints/src/methods/mod.rs @@ -102,6 +102,7 @@ mod single_char_add_str; mod single_char_insert_string; mod single_char_push_string; mod skip_while_next; +mod sliced_string_as_bytes; mod stable_sort_primitive; mod str_split; mod str_splitn; @@ -4363,6 +4364,34 @@ declare_clippy_lint! { "detect `repeat().take()` that can be replaced with `repeat_n()`" } +declare_clippy_lint! { + /// ### What it does + /// Checks for string slices immediantly followed by `as_bytes`. + /// + /// ### Why is this bad? + /// It involves doing an unnecessary UTF-8 alignment check which is less efficient, and can cause a panic. + /// + /// ### Known problems + /// In some cases, the UTF-8 validation and potential panic from string slicing may be required for + /// the code's correctness. If you need to ensure the slice boundaries fall on valid UTF-8 character + /// boundaries, the original form (`s[1..5].as_bytes()`) should be preferred. + /// + /// ### Example + /// ```rust + /// let s = "Lorem ipsum"; + /// s[1..5].as_bytes(); + /// ``` + /// Use instead: + /// ```rust + /// let s = "Lorem ipsum"; + /// &s.as_bytes()[1..5]; + /// ``` + #[clippy::version = "1.86.0"] + pub SLICED_STRING_AS_BYTES, + perf, + "slicing a string and immediately calling as_bytes is less efficient and can lead to panics" +} + pub struct Methods { avoid_breaking_exported_api: bool, msrv: Msrv, @@ -4531,6 +4560,7 @@ impl_lint_pass!(Methods => [ DOUBLE_ENDED_ITERATOR_LAST, USELESS_NONZERO_NEW_UNCHECKED, MANUAL_REPEAT_N, + SLICED_STRING_AS_BYTES, ]); /// Extracts a method call name, args, and `Span` of the method name. @@ -4798,6 +4828,7 @@ impl Methods { if let Some(("as_str", recv, [], as_str_span, _)) = method_call(recv) { redundant_as_str::check(cx, expr, recv, as_str_span, span); } + sliced_string_as_bytes::check(cx, expr, recv); }, ("as_mut", []) => useless_asref::check(cx, expr, "as_mut", recv), ("as_ptr", []) => manual_c_str_literals::check_as_ptr(cx, expr, recv, &self.msrv), diff --git a/clippy_lints/src/methods/sliced_string_as_bytes.rs b/clippy_lints/src/methods/sliced_string_as_bytes.rs new file mode 100644 index 000000000000..6d4cfdb34f31 --- /dev/null +++ b/clippy_lints/src/methods/sliced_string_as_bytes.rs @@ -0,0 +1,29 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_lang_item; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, LangItem, is_range_literal}; +use rustc_lint::LateContext; + +use super::SLICED_STRING_AS_BYTES; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>) { + if let ExprKind::Index(indexed, index, _) = recv.kind + && is_range_literal(index) + && let ty = cx.typeck_results().expr_ty(indexed).peel_refs() + && (ty.is_str() || is_type_lang_item(cx, ty, LangItem::String)) + { + let mut applicability = Applicability::MaybeIncorrect; + let stringish = snippet_with_applicability(cx, indexed.span, "_", &mut applicability); + let range = snippet_with_applicability(cx, index.span, "_", &mut applicability); + span_lint_and_sugg( + cx, + SLICED_STRING_AS_BYTES, + expr.span, + "calling `as_bytes` after slicing a string", + "try", + format!("&{stringish}.as_bytes()[{range}]"), + applicability, + ); + } +} diff --git a/tests/ui/bytes_nth.fixed b/tests/ui/bytes_nth.fixed index 11deb2390fd4..da35fcb55e5a 100644 --- a/tests/ui/bytes_nth.fixed +++ b/tests/ui/bytes_nth.fixed @@ -1,4 +1,5 @@ #![allow(clippy::unnecessary_operation)] +#![allow(clippy::sliced_string_as_bytes)] #![warn(clippy::bytes_nth)] fn main() { diff --git a/tests/ui/bytes_nth.rs b/tests/ui/bytes_nth.rs index 62d9c7a5ea79..5dbe84ecec8b 100644 --- a/tests/ui/bytes_nth.rs +++ b/tests/ui/bytes_nth.rs @@ -1,4 +1,5 @@ #![allow(clippy::unnecessary_operation)] +#![allow(clippy::sliced_string_as_bytes)] #![warn(clippy::bytes_nth)] fn main() { diff --git a/tests/ui/bytes_nth.stderr b/tests/ui/bytes_nth.stderr index c6f21576c3db..c5f341cb37f6 100644 --- a/tests/ui/bytes_nth.stderr +++ b/tests/ui/bytes_nth.stderr @@ -1,5 +1,5 @@ error: called `.bytes().nth()` on a `String` - --> tests/ui/bytes_nth.rs:6:13 + --> tests/ui/bytes_nth.rs:7:13 | LL | let _ = s.bytes().nth(3); | ^^^^^^^^^^^^^^^^ help: try: `s.as_bytes().get(3).copied()` @@ -8,13 +8,13 @@ LL | let _ = s.bytes().nth(3); = help: to override `-D warnings` add `#[allow(clippy::bytes_nth)]` error: called `.bytes().nth().unwrap()` on a `String` - --> tests/ui/bytes_nth.rs:7:14 + --> tests/ui/bytes_nth.rs:8:14 | LL | let _ = &s.bytes().nth(3).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `s.as_bytes()[3]` error: called `.bytes().nth()` on a `str` - --> tests/ui/bytes_nth.rs:8:13 + --> tests/ui/bytes_nth.rs:9:13 | LL | let _ = s[..].bytes().nth(3); | ^^^^^^^^^^^^^^^^^^^^ help: try: `s[..].as_bytes().get(3).copied()` diff --git a/tests/ui/sliced_string_as_bytes.fixed b/tests/ui/sliced_string_as_bytes.fixed new file mode 100644 index 000000000000..469ad27a99b9 --- /dev/null +++ b/tests/ui/sliced_string_as_bytes.fixed @@ -0,0 +1,34 @@ +#![allow(unused)] +#![warn(clippy::sliced_string_as_bytes)] + +use std::ops::{Index, Range}; + +struct Foo; + +struct Bar; + +impl Bar { + fn as_bytes(&self) -> &[u8] { + &[0, 1, 2, 3] + } +} + +impl Index> for Foo { + type Output = Bar; + + fn index(&self, _: Range) -> &Self::Output { + &Bar + } +} + +fn main() { + let s = "Lorem ipsum"; + let string: String = "dolor sit amet".to_owned(); + + let bytes = &s.as_bytes()[1..5]; + let bytes = &string.as_bytes()[1..]; + let bytes = &"consectetur adipiscing".as_bytes()[..=5]; + + let f = Foo; + let bytes = f[0..4].as_bytes(); +} diff --git a/tests/ui/sliced_string_as_bytes.rs b/tests/ui/sliced_string_as_bytes.rs new file mode 100644 index 000000000000..4a4605e5a1ae --- /dev/null +++ b/tests/ui/sliced_string_as_bytes.rs @@ -0,0 +1,34 @@ +#![allow(unused)] +#![warn(clippy::sliced_string_as_bytes)] + +use std::ops::{Index, Range}; + +struct Foo; + +struct Bar; + +impl Bar { + fn as_bytes(&self) -> &[u8] { + &[0, 1, 2, 3] + } +} + +impl Index> for Foo { + type Output = Bar; + + fn index(&self, _: Range) -> &Self::Output { + &Bar + } +} + +fn main() { + let s = "Lorem ipsum"; + let string: String = "dolor sit amet".to_owned(); + + let bytes = s[1..5].as_bytes(); + let bytes = string[1..].as_bytes(); + let bytes = "consectetur adipiscing"[..=5].as_bytes(); + + let f = Foo; + let bytes = f[0..4].as_bytes(); +} diff --git a/tests/ui/sliced_string_as_bytes.stderr b/tests/ui/sliced_string_as_bytes.stderr new file mode 100644 index 000000000000..1342f4c01a48 --- /dev/null +++ b/tests/ui/sliced_string_as_bytes.stderr @@ -0,0 +1,23 @@ +error: calling `as_bytes` after slicing a string + --> tests/ui/sliced_string_as_bytes.rs:28:17 + | +LL | let bytes = s[1..5].as_bytes(); + | ^^^^^^^^^^^^^^^^^^ help: try: `&s.as_bytes()[1..5]` + | + = note: `-D clippy::sliced-string-as-bytes` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::sliced_string_as_bytes)]` + +error: calling `as_bytes` after slicing a string + --> tests/ui/sliced_string_as_bytes.rs:29:17 + | +LL | let bytes = string[1..].as_bytes(); + | ^^^^^^^^^^^^^^^^^^^^^^ help: try: `&string.as_bytes()[1..]` + +error: calling `as_bytes` after slicing a string + --> tests/ui/sliced_string_as_bytes.rs:30:17 + | +LL | let bytes = "consectetur adipiscing"[..=5].as_bytes(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `&"consectetur adipiscing".as_bytes()[..=5]` + +error: aborting due to 3 previous errors +