Allow is and is not for direct type comparisons (#7905)

## Summary

This PR updates our E721 implementation and semantics to match the
updated `pycodestyle` logic, which I think is an improvement.
Specifically, we now allow `type(obj) is int` for exact type
comparisons, which were previously impossible. So now, we're largely
just linting against code like `type(obj) == int`.

This change is gated to preview mode.

Closes https://github.com/astral-sh/ruff/issues/7904.

## Test Plan

Updated the test fixture and ensured parity with latest Flake8.
This commit is contained in:
Charlie Marsh 2023-10-20 19:27:12 -04:00 committed by GitHub
parent f6d6200aae
commit df807ff912
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 329 additions and 105 deletions

View file

@ -4,18 +4,18 @@ if type(res) == type(42):
#: E721 #: E721
if type(res) != type(""): if type(res) != type(""):
pass pass
#: E721 #: Okay
import types import types
if res == types.IntType: if res == types.IntType:
pass pass
#: E721 #: Okay
import types import types
if type(res) is not types.ListType: if type(res) is not types.ListType:
pass pass
#: E721 #: E721
assert type(res) == type(False) assert type(res) == type(False) or type(res) == type(None)
#: E721 #: E721
assert type(res) == type([]) assert type(res) == type([])
#: E721 #: E721
@ -25,21 +25,18 @@ assert type(res) == type((0,))
#: E721 #: E721
assert type(res) == type((0)) assert type(res) == type((0))
#: E721 #: E721
assert type(res) != type((1,)) assert type(res) != type((1, ))
#: E721 #: Okay
assert type(res) is type((1,)) assert type(res) is type((1, ))
#: E721 #: Okay
assert type(res) is not type((1,)) assert type(res) is not type((1, ))
#: E211 E721 #: E211 E721
assert type(res) == type( assert type(res) == type ([2, ])
[
2,
]
)
#: E201 E201 E202 E721 #: E201 E201 E202 E721
assert type(res) == type(()) assert type(res) == type( ( ) )
#: E201 E202 E721 #: E201 E202 E721
assert type(res) == type((0,)) assert type(res) == type( (0, ) )
#:
#: Okay #: Okay
import types import types
@ -50,17 +47,47 @@ if isinstance(res, str):
pass pass
if isinstance(res, types.MethodType): if isinstance(res, types.MethodType):
pass pass
if type(a) != type(b) or type(a) == type(ccc): #: Okay
def func_histype(a, b, c):
pass pass
#: E722
assert type(res) == type(None) try:
types = StrEnum
if x == types.X:
pass pass
except:
pass
#: E722
try:
pass
except Exception:
pass
except:
pass
#: E722 E203 E271
try:
pass
except :
pass
#: Okay
fake_code = """"
try:
do_something()
except:
pass
"""
try:
pass
except Exception:
pass
#: Okay
from . import custom_types as types
#: E721 red = types.ColorTypeRED
assert type(res) is int red is types.ColorType.RED
#: Okay
from . import compute_type
if compute_type(foo) == 5:
pass
class Foo: class Foo:

View file

@ -16,6 +16,7 @@ mod tests {
use crate::line_width::LineLength; use crate::line_width::LineLength;
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path; use crate::test::test_path;
use crate::{assert_messages, settings}; use crate::{assert_messages, settings};
@ -61,6 +62,24 @@ mod tests {
Ok(()) Ok(())
} }
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pycodestyle").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test] #[test]
fn w292_4() -> Result<()> { fn w292_4() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(

View file

@ -3,26 +3,31 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none; use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::settings::types::PreviewMode;
/// ## What it does /// ## What it does
/// Checks for object type comparisons without using `isinstance()`. /// Checks for object type comparisons using `==` and other comparison
/// operators.
/// ///
/// ## Why is this bad? /// ## Why is this bad?
/// Do not compare types directly. /// Unlike a direct type comparison, `isinstance` will also check if an object
/// is an instance of a class or a subclass thereof.
/// ///
/// When checking if an object is a instance of a certain type, keep in mind /// Under [preview mode](https://docs.astral.sh/ruff/preview), this rule also
/// that it might be subclassed. For example, `bool` inherits from `int`, and /// allows for direct type comparisons using `is` and `is not`, to check for
/// `Exception` inherits from `BaseException`. /// exact type equality (while still forbidding comparisons using `==` and
/// `!=`).
/// ///
/// ## Example /// ## Example
/// ```python /// ```python
/// if type(obj) is type(1): /// if type(obj) == type(1):
/// pass /// pass
/// ///
/// if type(obj) is int: /// if type(obj) == int:
/// pass /// pass
/// ``` /// ```
/// ///
@ -32,17 +37,31 @@ use crate::checkers::ast::Checker;
/// pass /// pass
/// ``` /// ```
#[violation] #[violation]
pub struct TypeComparison; pub struct TypeComparison {
preview: PreviewMode,
}
impl Violation for TypeComparison { impl Violation for TypeComparison {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
format!("Do not compare types, use `isinstance()`") match self.preview {
PreviewMode::Disabled => format!("Do not compare types, use `isinstance()`"),
PreviewMode::Enabled => format!(
"Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks"
),
}
} }
} }
/// E721 /// E721
pub(crate) fn type_comparison(checker: &mut Checker, compare: &ast::ExprCompare) { pub(crate) fn type_comparison(checker: &mut Checker, compare: &ast::ExprCompare) {
match checker.settings.preview {
PreviewMode::Disabled => deprecated_type_comparison(checker, compare),
PreviewMode::Enabled => preview_type_comparison(checker, compare),
}
}
fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare) {
for ((left, right), op) in std::iter::once(compare.left.as_ref()) for ((left, right), op) in std::iter::once(compare.left.as_ref())
.chain(compare.comparators.iter()) .chain(compare.comparators.iter())
.tuple_windows() .tuple_windows()
@ -82,9 +101,12 @@ pub(crate) fn type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
.first() .first()
.is_some_and(|arg| !arg.is_name_expr() && !is_const_none(arg)) .is_some_and(|arg| !arg.is_name_expr() && !is_const_none(arg))
{ {
checker checker.diagnostics.push(Diagnostic::new(
.diagnostics TypeComparison {
.push(Diagnostic::new(TypeComparison, compare.range())); preview: PreviewMode::Disabled,
},
compare.range(),
));
} }
} }
} }
@ -95,9 +117,12 @@ pub(crate) fn type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
.resolve_call_path(value.as_ref()) .resolve_call_path(value.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["types", ..])) .is_some_and(|call_path| matches!(call_path.as_slice(), ["types", ..]))
{ {
checker checker.diagnostics.push(Diagnostic::new(
.diagnostics TypeComparison {
.push(Diagnostic::new(TypeComparison, compare.range())); preview: PreviewMode::Disabled,
},
compare.range(),
));
} }
} }
Expr::Name(ast::ExprName { id, .. }) => { Expr::Name(ast::ExprName { id, .. }) => {
@ -115,12 +140,66 @@ pub(crate) fn type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
| "set" | "set"
) && checker.semantic().is_builtin(id) ) && checker.semantic().is_builtin(id)
{ {
checker checker.diagnostics.push(Diagnostic::new(
.diagnostics TypeComparison {
.push(Diagnostic::new(TypeComparison, compare.range())); preview: PreviewMode::Disabled,
},
compare.range(),
));
} }
} }
_ => {} _ => {}
} }
} }
} }
pub(crate) fn preview_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare) {
for (left, right) in std::iter::once(compare.left.as_ref())
.chain(compare.comparators.iter())
.tuple_windows()
.zip(compare.ops.iter())
.filter(|(_, op)| matches!(op, CmpOp::Eq | CmpOp::NotEq))
.map(|((left, right), _)| (left, right))
{
if is_type(left, checker.semantic()) || is_type(right, checker.semantic()) {
checker.diagnostics.push(Diagnostic::new(
TypeComparison {
preview: PreviewMode::Enabled,
},
compare.range(),
));
}
}
}
/// Returns `true` if the [`Expr`] is known to evaluate to a type (e.g., `int`, or `type(1)`).
fn is_type(expr: &Expr, semantic: &SemanticModel) -> bool {
match expr {
Expr::Call(ast::ExprCall {
func, arguments, ..
}) => {
// Ex) `type(obj) == type(1)`
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return false;
};
if !(id == "type" && semantic.is_builtin("type")) {
return false;
};
// Allow comparison for types which are not obvious.
arguments
.args
.first()
.is_some_and(|arg| !arg.is_name_expr() && !is_const_none(arg))
}
Expr::Name(ast::ExprName { id, .. }) => {
// Ex) `type(obj) == int`
matches!(
id.as_str(),
"int" | "str" | "float" | "bool" | "complex" | "bytes" | "list" | "dict" | "set"
) && semantic.is_builtin(id)
}
_ => false,
}
}

View file

@ -17,7 +17,7 @@ E721.py:5:4: E721 Do not compare types, use `isinstance()`
5 | if type(res) != type(""): 5 | if type(res) != type(""):
| ^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^ E721
6 | pass 6 | pass
7 | #: E721 7 | #: Okay
| |
E721.py:15:4: E721 Do not compare types, use `isinstance()` E721.py:15:4: E721 Do not compare types, use `isinstance()`
@ -34,7 +34,7 @@ E721.py:18:8: E721 Do not compare types, use `isinstance()`
| |
16 | pass 16 | pass
17 | #: E721 17 | #: E721
18 | assert type(res) == type(False) 18 | assert type(res) == type(False) or type(res) == type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^^^^ E721
19 | #: E721 19 | #: E721
20 | assert type(res) == type([]) 20 | assert type(res) == type([])
@ -42,7 +42,7 @@ E721.py:18:8: E721 Do not compare types, use `isinstance()`
E721.py:20:8: E721 Do not compare types, use `isinstance()` E721.py:20:8: E721 Do not compare types, use `isinstance()`
| |
18 | assert type(res) == type(False) 18 | assert type(res) == type(False) or type(res) == type(None)
19 | #: E721 19 | #: E721
20 | assert type(res) == type([]) 20 | assert type(res) == type([])
| ^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^ E721
@ -77,97 +77,84 @@ E721.py:26:8: E721 Do not compare types, use `isinstance()`
26 | assert type(res) == type((0)) 26 | assert type(res) == type((0))
| ^^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^^ E721
27 | #: E721 27 | #: E721
28 | assert type(res) != type((1,)) 28 | assert type(res) != type((1, ))
| |
E721.py:28:8: E721 Do not compare types, use `isinstance()` E721.py:28:8: E721 Do not compare types, use `isinstance()`
| |
26 | assert type(res) == type((0)) 26 | assert type(res) == type((0))
27 | #: E721 27 | #: E721
28 | assert type(res) != type((1,)) 28 | assert type(res) != type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^^^^ E721
29 | #: E721 29 | #: Okay
30 | assert type(res) is type((1,)) 30 | assert type(res) is type((1, ))
| |
E721.py:30:8: E721 Do not compare types, use `isinstance()` E721.py:30:8: E721 Do not compare types, use `isinstance()`
| |
28 | assert type(res) != type((1,)) 28 | assert type(res) != type((1, ))
29 | #: E721 29 | #: Okay
30 | assert type(res) is type((1,)) 30 | assert type(res) is type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^^^^ E721
31 | #: E721 31 | #: Okay
32 | assert type(res) is not type((1,)) 32 | assert type(res) is not type((1, ))
| |
E721.py:32:8: E721 Do not compare types, use `isinstance()` E721.py:32:8: E721 Do not compare types, use `isinstance()`
| |
30 | assert type(res) is type((1,)) 30 | assert type(res) is type((1, ))
31 | #: E721 31 | #: Okay
32 | assert type(res) is not type((1,)) 32 | assert type(res) is not type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
33 | #: E211 E721 33 | #: E211 E721
34 | assert type(res) == type( 34 | assert type(res) == type ([2, ])
| |
E721.py:34:8: E721 Do not compare types, use `isinstance()` E721.py:34:8: E721 Do not compare types, use `isinstance()`
| |
32 | assert type(res) is not type((1,)) 32 | assert type(res) is not type((1, ))
33 | #: E211 E721 33 | #: E211 E721
34 | assert type(res) == type( 34 | assert type(res) == type ([2, ])
| ________^ | ^^^^^^^^^^^^^^^^^^^^^^^^^ E721
35 | | [ 35 | #: E201 E201 E202 E721
36 | | 2, 36 | assert type(res) == type( ( ) )
37 | | ]
38 | | )
| |_^ E721
39 | #: E201 E201 E202 E721
40 | assert type(res) == type(())
| |
E721.py:40:8: E721 Do not compare types, use `isinstance()` E721.py:36:8: E721 Do not compare types, use `isinstance()`
| |
38 | ) 34 | assert type(res) == type ([2, ])
39 | #: E201 E201 E202 E721 35 | #: E201 E201 E202 E721
40 | assert type(res) == type(()) 36 | assert type(res) == type( ( ) )
| ^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^^^^ E721
41 | #: E201 E202 E721 37 | #: E201 E202 E721
42 | assert type(res) == type((0,)) 38 | assert type(res) == type( (0, ) )
| |
E721.py:42:8: E721 Do not compare types, use `isinstance()` E721.py:38:8: E721 Do not compare types, use `isinstance()`
| |
40 | assert type(res) == type(()) 36 | assert type(res) == type( ( ) )
41 | #: E201 E202 E721 37 | #: E201 E202 E721
42 | assert type(res) == type((0,)) 38 | assert type(res) == type( (0, ) )
| ^^^^^^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
43 | 39 | #:
44 | #: Okay
| |
E721.py:63:8: E721 Do not compare types, use `isinstance()` E721.py:96:12: E721 Do not compare types, use `isinstance()`
| |
62 | #: E721 94 | def asdf(self, value: str | None):
63 | assert type(res) is int 95 | #: E721
| ^^^^^^^^^^^^^^^^ E721 96 | if type(value) is str:
|
E721.py:69:12: E721 Do not compare types, use `isinstance()`
|
67 | def asdf(self, value: str | None):
68 | #: E721
69 | if type(value) is str:
| ^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^ E721
70 | ... 97 | ...
| |
E721.py:79:12: E721 Do not compare types, use `isinstance()` E721.py:106:12: E721 Do not compare types, use `isinstance()`
| |
77 | def asdf(self, value: str | None): 104 | def asdf(self, value: str | None):
78 | #: E721 105 | #: E721
79 | if type(value) is str: 106 | if type(value) is str:
| ^^^^^^^^^^^^^^^^^^ E721 | ^^^^^^^^^^^^^^^^^^ E721
80 | ... 107 | ...
| |

View file

@ -0,0 +1,112 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E721.py:2:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
1 | #: E721
2 | if type(res) == type(42):
| ^^^^^^^^^^^^^^^^^^^^^ E721
3 | pass
4 | #: E721
|
E721.py:5:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
3 | pass
4 | #: E721
5 | if type(res) != type(""):
| ^^^^^^^^^^^^^^^^^^^^^ E721
6 | pass
7 | #: Okay
|
E721.py:18:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
16 | pass
17 | #: E721
18 | assert type(res) == type(False) or type(res) == type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
19 | #: E721
20 | assert type(res) == type([])
|
E721.py:20:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
18 | assert type(res) == type(False) or type(res) == type(None)
19 | #: E721
20 | assert type(res) == type([])
| ^^^^^^^^^^^^^^^^^^^^^ E721
21 | #: E721
22 | assert type(res) == type(())
|
E721.py:22:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
20 | assert type(res) == type([])
21 | #: E721
22 | assert type(res) == type(())
| ^^^^^^^^^^^^^^^^^^^^^ E721
23 | #: E721
24 | assert type(res) == type((0,))
|
E721.py:24:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
22 | assert type(res) == type(())
23 | #: E721
24 | assert type(res) == type((0,))
| ^^^^^^^^^^^^^^^^^^^^^^^ E721
25 | #: E721
26 | assert type(res) == type((0))
|
E721.py:26:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
24 | assert type(res) == type((0,))
25 | #: E721
26 | assert type(res) == type((0))
| ^^^^^^^^^^^^^^^^^^^^^^ E721
27 | #: E721
28 | assert type(res) != type((1, ))
|
E721.py:28:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
26 | assert type(res) == type((0))
27 | #: E721
28 | assert type(res) != type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
29 | #: Okay
30 | assert type(res) is type((1, ))
|
E721.py:34:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
32 | assert type(res) is not type((1, ))
33 | #: E211 E721
34 | assert type(res) == type ([2, ])
| ^^^^^^^^^^^^^^^^^^^^^^^^^ E721
35 | #: E201 E201 E202 E721
36 | assert type(res) == type( ( ) )
|
E721.py:36:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
34 | assert type(res) == type ([2, ])
35 | #: E201 E201 E202 E721
36 | assert type(res) == type( ( ) )
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
37 | #: E201 E202 E721
38 | assert type(res) == type( (0, ) )
|
E721.py:38:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
36 | assert type(res) == type( ( ) )
37 | #: E201 E202 E721
38 | assert type(res) == type( (0, ) )
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
39 | #:
|