From 96913d061f2a400b374adbd9ea13826560166745 Mon Sep 17 00:00:00 2001 From: ABohra3 Date: Tue, 14 Apr 2026 21:08:27 -0500 Subject: [PATCH 1/2] Add unnecessary-type-conversion lint for str/int/float (fixes #3109) --- crates/pyrefly_config/src/error_kind.rs | 3 + pyrefly/lib/alt/call.rs | 32 +++++++++ pyrefly/lib/test/mod.rs | 1 + .../lib/test/unnecessary_type_conversion.rs | 65 +++++++++++++++++++ website/docs/error-kinds.mdx | 11 ++++ 5 files changed, 112 insertions(+) create mode 100644 pyrefly/lib/test/unnecessary_type_conversion.rs diff --git a/crates/pyrefly_config/src/error_kind.rs b/crates/pyrefly_config/src/error_kind.rs index 126d61f679..4d7141f217 100644 --- a/crates/pyrefly_config/src/error_kind.rs +++ b/crates/pyrefly_config/src/error_kind.rs @@ -276,6 +276,8 @@ pub enum ErrorKind { /// Identity comparison (`is` or `is not`) between types that are provably disjoint /// or between literals whose comparison result is statically known. UnnecessaryComparison, + /// Warning when calling a builtin type constructor (str, int, float, bool) on a value that is already of that type. + UnnecessaryTypeConversion, /// A return or yield that can never be reached. /// This occurs when a return/yield follows a statement that always exits, /// such as return, raise, break, or continue. @@ -359,6 +361,7 @@ impl ErrorKind { ErrorKind::UnannotatedParameter => Severity::Ignore, ErrorKind::UnannotatedReturn => Severity::Ignore, ErrorKind::UnnecessaryComparison => Severity::Warn, + ErrorKind::UnnecessaryTypeConversion => Severity::Warn, ErrorKind::Unreachable => Severity::Warn, ErrorKind::UnresolvableDunderAll => Severity::Warn, ErrorKind::UntypedImport => Severity::Warn, diff --git a/pyrefly/lib/alt/call.rs b/pyrefly/lib/alt/call.rs index c89e8f0092..98ba784573 100644 --- a/pyrefly/lib/alt/call.rs +++ b/pyrefly/lib/alt/call.rs @@ -1000,6 +1000,37 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } + fn check_unnecessary_type_conversion( + &self, + cls: &ClassType, + args: &[CallArg], + range: TextRange, + errors: &ErrorCollector, + ) { + let builtin_names = ["str", "int", "float"]; + if !builtin_names + .iter() + .any(|name| cls.has_qname("builtins", name)) + { + return; + } + if let Some(arg_ty) = self.first_arg_type(args, errors) { + let target_ty = self.heap.mk_class_type(cls.clone()); + if !arg_ty.is_any() && arg_ty == target_ty { + self.error( + errors, + range, + ErrorInfo::Kind(ErrorKind::UnnecessaryTypeConversion), + format!( + "Unnecessary `{}()` call; argument is already of type `{}`", + cls.name(), + arg_ty.deterministic_printing(), + ), + ); + } + } + } + fn call_infer_with_callee_range( &self, call_target: CallTarget, @@ -1092,6 +1123,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } }; + self.check_unnecessary_type_conversion(&cls, args, arguments_range, errors); let constructed_type = self.construct_class( cls, constructor_kind, diff --git a/pyrefly/lib/test/mod.rs b/pyrefly/lib/test/mod.rs index 0b02aa7729..77d2ed98db 100644 --- a/pyrefly/lib/test/mod.rs +++ b/pyrefly/lib/test/mod.rs @@ -78,6 +78,7 @@ mod typed_dict; mod typeform; mod typing_self; mod unnecessary_comparison; +mod unnecessary_type_conversion; mod untyped_def_behaviors; pub mod util; mod var_resolution; diff --git a/pyrefly/lib/test/unnecessary_type_conversion.rs b/pyrefly/lib/test/unnecessary_type_conversion.rs new file mode 100644 index 0000000000..5fc493e4b8 --- /dev/null +++ b/pyrefly/lib/test/unnecessary_type_conversion.rs @@ -0,0 +1,65 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use crate::testcase; + +testcase!( + test_str_to_str, + r#" +def f(x: str) -> None: + y = str(x) # E: Unnecessary `str()` call; argument is already of type `str` +"#, +); + +testcase!( + test_int_to_int, + r#" +def f(x: int) -> None: + y = int(x) # E: Unnecessary `int()` call; argument is already of type `int` +"#, +); + +testcase!( + test_float_to_float, + r#" +def f(x: float) -> None: + y = float(x) # E: Unnecessary `float()` call; argument is already of type `float` +"#, +); + +testcase!( + test_int_to_str_ok, + r#" +def f(x: int) -> None: + y = str(x) # OK - converting int to str +"#, +); + +testcase!( + test_bool_to_int_ok, + r#" +def f(x: bool) -> None: + y = int(x) # OK - bool is a subtype of int, types are not equal +"#, +); + +testcase!( + test_any_ok, + r#" +from typing import Any +def f(x: Any) -> None: + y = str(x) # OK - argument is Any, type is unknown +"#, +); + +testcase!( + test_no_args_ok, + r#" +def f() -> None: + y = str() # OK - no argument +"#, +); diff --git a/website/docs/error-kinds.mdx b/website/docs/error-kinds.mdx index 2b98082fbb..8fe91d8519 100644 --- a/website/docs/error-kinds.mdx +++ b/website/docs/error-kinds.mdx @@ -1372,6 +1372,17 @@ def test1(x: object) -> None: This check is relatively conservative and only warns on limited cases where the comparison is highly likely to be redundant. +## unnecessary-type-conversion + +This warning is raised when a builtin type constructor (`str`, `int`, `float`, or `bool`) is called on a value that is already of that type, making the conversion redundant. + +The default severity of this diagnostic is `warn`. + +```python +def f(x: str) -> None: + y = str(x) # unnecessary-type-conversion: `x` is already of type `str` +``` + ## unreachable This error is raised when a `return` or `yield` can never be reached because it comes From 141bfa8997cbbf9d88a28b303c1aa1eef5686059 Mon Sep 17 00:00:00 2001 From: ABohra3 Date: Wed, 15 Apr 2026 19:34:03 -0500 Subject: [PATCH 2/2] Extend unnecessary-type-conversion lint to redundant bytes() calls. --- crates/pyrefly_config/src/error_kind.rs | 2 +- pyrefly/lib/alt/call.rs | 2 +- pyrefly/lib/test/unnecessary_type_conversion.rs | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/pyrefly_config/src/error_kind.rs b/crates/pyrefly_config/src/error_kind.rs index 4d7141f217..671a29be35 100644 --- a/crates/pyrefly_config/src/error_kind.rs +++ b/crates/pyrefly_config/src/error_kind.rs @@ -276,7 +276,7 @@ pub enum ErrorKind { /// Identity comparison (`is` or `is not`) between types that are provably disjoint /// or between literals whose comparison result is statically known. UnnecessaryComparison, - /// Warning when calling a builtin type constructor (str, int, float, bool) on a value that is already of that type. + /// Warning when calling a builtin type constructor (str, int, float, bytes) on a value that is already of that type. UnnecessaryTypeConversion, /// A return or yield that can never be reached. /// This occurs when a return/yield follows a statement that always exits, diff --git a/pyrefly/lib/alt/call.rs b/pyrefly/lib/alt/call.rs index 98ba784573..5926e1530a 100644 --- a/pyrefly/lib/alt/call.rs +++ b/pyrefly/lib/alt/call.rs @@ -1007,7 +1007,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { range: TextRange, errors: &ErrorCollector, ) { - let builtin_names = ["str", "int", "float"]; + let builtin_names = ["str", "int", "float", "bytes"]; if !builtin_names .iter() .any(|name| cls.has_qname("builtins", name)) diff --git a/pyrefly/lib/test/unnecessary_type_conversion.rs b/pyrefly/lib/test/unnecessary_type_conversion.rs index 5fc493e4b8..b9455bb732 100644 --- a/pyrefly/lib/test/unnecessary_type_conversion.rs +++ b/pyrefly/lib/test/unnecessary_type_conversion.rs @@ -56,6 +56,14 @@ def f(x: Any) -> None: "#, ); +testcase!( + test_bytes_to_bytes, + r#" +def f(x: bytes) -> None: + y = bytes(x) # E: Unnecessary `bytes()` call; argument is already of type `bytes` +"#, +); + testcase!( test_no_args_ok, r#"