Skip to content

Commit 363c4e0

Browse files
committed
Add unnecessary-type-conversion lint for str/int/float (fixes #3109)
1 parent 9ebf6bc commit 363c4e0

5 files changed

Lines changed: 112 additions & 0 deletions

File tree

crates/pyrefly_config/src/error_kind.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ pub enum ErrorKind {
276276
/// Identity comparison (`is` or `is not`) between types that are provably disjoint
277277
/// or between literals whose comparison result is statically known.
278278
UnnecessaryComparison,
279+
/// Warning when calling a builtin type constructor (str, int, float, bool) on a value that is already of that type.
280+
UnnecessaryTypeConversion,
279281
/// A return or yield that can never be reached.
280282
/// This occurs when a return/yield follows a statement that always exits,
281283
/// such as return, raise, break, or continue.
@@ -359,6 +361,7 @@ impl ErrorKind {
359361
ErrorKind::UnannotatedParameter => Severity::Ignore,
360362
ErrorKind::UnannotatedReturn => Severity::Ignore,
361363
ErrorKind::UnnecessaryComparison => Severity::Warn,
364+
ErrorKind::UnnecessaryTypeConversion => Severity::Warn,
362365
ErrorKind::Unreachable => Severity::Warn,
363366
ErrorKind::UnresolvableDunderAll => Severity::Warn,
364367
ErrorKind::UntypedImport => Severity::Warn,

pyrefly/lib/alt/call.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,37 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
10001000
}
10011001
}
10021002

1003+
fn check_unnecessary_type_conversion(
1004+
&self,
1005+
cls: &ClassType,
1006+
args: &[CallArg],
1007+
range: TextRange,
1008+
errors: &ErrorCollector,
1009+
) {
1010+
let builtin_names = ["str", "int", "float"];
1011+
if !builtin_names
1012+
.iter()
1013+
.any(|name| cls.has_qname("builtins", name))
1014+
{
1015+
return;
1016+
}
1017+
if let Some(arg_ty) = self.first_arg_type(args, errors) {
1018+
let target_ty = self.heap.mk_class_type(cls.clone());
1019+
if !arg_ty.is_any() && arg_ty == target_ty {
1020+
self.error(
1021+
errors,
1022+
range,
1023+
ErrorInfo::Kind(ErrorKind::UnnecessaryTypeConversion),
1024+
format!(
1025+
"Unnecessary `{}()` call; argument is already of type `{}`",
1026+
cls.name(),
1027+
arg_ty.deterministic_printing(),
1028+
),
1029+
);
1030+
}
1031+
}
1032+
}
1033+
10031034
fn call_infer_with_callee_range(
10041035
&self,
10051036
call_target: CallTarget,
@@ -1092,6 +1123,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
10921123
}
10931124
}
10941125
};
1126+
self.check_unnecessary_type_conversion(&cls, args, arguments_range, errors);
10951127
let constructed_type = self.construct_class(
10961128
cls,
10971129
constructor_kind,

pyrefly/lib/test/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ mod typed_dict;
7878
mod typeform;
7979
mod typing_self;
8080
mod unnecessary_comparison;
81+
mod unnecessary_type_conversion;
8182
mod untyped_def_behaviors;
8283
pub mod util;
8384
mod var_resolution;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use crate::testcase;
9+
10+
testcase!(
11+
test_str_to_str,
12+
r#"
13+
def f(x: str) -> None:
14+
y = str(x) # E: Unnecessary `str()` call; argument is already of type `str`
15+
"#,
16+
);
17+
18+
testcase!(
19+
test_int_to_int,
20+
r#"
21+
def f(x: int) -> None:
22+
y = int(x) # E: Unnecessary `int()` call; argument is already of type `int`
23+
"#,
24+
);
25+
26+
testcase!(
27+
test_float_to_float,
28+
r#"
29+
def f(x: float) -> None:
30+
y = float(x) # E: Unnecessary `float()` call; argument is already of type `float`
31+
"#,
32+
);
33+
34+
testcase!(
35+
test_int_to_str_ok,
36+
r#"
37+
def f(x: int) -> None:
38+
y = str(x) # OK - converting int to str
39+
"#,
40+
);
41+
42+
testcase!(
43+
test_bool_to_int_ok,
44+
r#"
45+
def f(x: bool) -> None:
46+
y = int(x) # OK - bool is a subtype of int, types are not equal
47+
"#,
48+
);
49+
50+
testcase!(
51+
test_any_ok,
52+
r#"
53+
from typing import Any
54+
def f(x: Any) -> None:
55+
y = str(x) # OK - argument is Any, type is unknown
56+
"#,
57+
);
58+
59+
testcase!(
60+
test_no_args_ok,
61+
r#"
62+
def f() -> None:
63+
y = str() # OK - no argument
64+
"#,
65+
);

website/docs/error-kinds.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,17 @@ def test1(x: object) -> None:
13721372

13731373
This check is relatively conservative and only warns on limited cases where the comparison is highly likely to be redundant.
13741374

1375+
## unnecessary-type-conversion
1376+
1377+
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.
1378+
1379+
The default severity of this diagnostic is `warn`.
1380+
1381+
```python
1382+
def f(x: str) -> None:
1383+
y = str(x) # unnecessary-type-conversion: `x` is already of type `str`
1384+
```
1385+
13751386
## unreachable
13761387

13771388
This error is raised when a `return` or `yield` can never be reached because it comes

0 commit comments

Comments
 (0)