-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[
ruff
] Add rule forbidding `map(int, package.__version__.split('.')…
…)` (`RUF048`) (#14373) Co-authored-by: Alex Waygood <[email protected]>
- Loading branch information
1 parent
1f07880
commit 3642381
Showing
10 changed files
with
277 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
__version__ = (0, 1, 0) | ||
|
||
|
||
tuple(map(int, __version__.split("."))) | ||
list(map(int, __version__.split("."))) | ||
|
||
# `sep` passed as keyword argument | ||
for part in map(int, __version__.split(sep=".")): | ||
print(part) | ||
|
||
# Comma | ||
tuple(map(int, __version__.split(","))) | ||
list(map(int, __version__.split(","))) | ||
|
||
# Multiple arguments | ||
tuple(map(int, __version__.split(".", 1))) | ||
list(map(int, __version__.split(".", maxsplit=2))) |
27 changes: 27 additions & 0 deletions
27
crates/ruff_linter/resources/test/fixtures/ruff/RUF048_1.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from foo import __version__ | ||
import bar | ||
|
||
|
||
tuple(map(int, __version__.split("."))) | ||
list(map(int, __version__.split("."))) | ||
tuple(map(int, bar.__version__.split("."))) | ||
list(map(int, bar.__version__.split("."))) | ||
|
||
# `sep` passed as keyword argument | ||
for part in map(int, bar.__version__.split(sep=".")): | ||
print(part) | ||
|
||
for part in map(int, __version__.split(sep=".")): | ||
print(part) | ||
|
||
# Comma | ||
tuple(map(int, __version__.split(","))) | ||
list(map(int, __version__.split(","))) | ||
tuple(map(int, bar.__version__.split(","))) | ||
list(map(int, bar.__version__.split(","))) | ||
|
||
# Multiple arguments | ||
tuple(map(int, __version__.split(",", 1))) | ||
list(map(int, __version__.split(",", maxsplit = 2))) | ||
tuple(map(int, bar.__version__.split(",", 1))) | ||
list(map(int, bar.__version__.split(",", maxsplit = 2))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast as ast; | ||
use ruff_python_semantic::SemanticModel; | ||
use ruff_text_size::Ranged; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for calls of the form `map(int, __version__.split("."))`. | ||
/// | ||
/// ## Why is this bad? | ||
/// `__version__` does not always contain integral-like elements. | ||
/// | ||
/// ```python | ||
/// import matplotlib # `__version__ == "3.9.1.post-1"` in our environment | ||
/// | ||
/// # ValueError: invalid literal for int() with base 10: 'post1' | ||
/// tuple(map(int, matplotlib.__version__.split("."))) | ||
/// ``` | ||
/// | ||
/// See also [*Version specifiers* | Packaging spec][version-specifier]. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// tuple(map(int, matplotlib.__version__.split("."))) | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// import packaging.version as version | ||
/// | ||
/// version.parse(matplotlib.__version__) | ||
/// ``` | ||
/// | ||
/// [version-specifier]: https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers | ||
#[violation] | ||
pub struct MapIntVersionParsing; | ||
|
||
impl Violation for MapIntVersionParsing { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
"`__version__` may contain non-integral-like elements".to_string() | ||
} | ||
} | ||
|
||
/// RUF048 | ||
pub(crate) fn map_int_version_parsing(checker: &mut Checker, call: &ast::ExprCall) { | ||
let semantic = checker.semantic(); | ||
|
||
let Some((first, second)) = map_call_with_two_arguments(semantic, call) else { | ||
return; | ||
}; | ||
|
||
if is_dunder_version_split_dot(second) && semantic.match_builtin_expr(first, "int") { | ||
checker | ||
.diagnostics | ||
.push(Diagnostic::new(MapIntVersionParsing, call.range())); | ||
} | ||
} | ||
|
||
fn map_call_with_two_arguments<'a>( | ||
semantic: &SemanticModel, | ||
call: &'a ast::ExprCall, | ||
) -> Option<(&'a ast::Expr, &'a ast::Expr)> { | ||
let ast::ExprCall { | ||
func, | ||
arguments: | ||
ast::Arguments { | ||
args, | ||
keywords, | ||
range: _, | ||
}, | ||
range: _, | ||
} = call; | ||
|
||
if !keywords.is_empty() { | ||
return None; | ||
} | ||
|
||
let [first, second] = &**args else { | ||
return None; | ||
}; | ||
|
||
if !semantic.match_builtin_expr(func, "map") { | ||
return None; | ||
}; | ||
|
||
Some((first, second)) | ||
} | ||
|
||
/// Whether `expr` has the form `__version__.split(".")` or `something.__version__.split(".")`. | ||
fn is_dunder_version_split_dot(expr: &ast::Expr) -> bool { | ||
let ast::Expr::Call(ast::ExprCall { | ||
func, arguments, .. | ||
}) = expr | ||
else { | ||
return false; | ||
}; | ||
|
||
if arguments.len() != 1 { | ||
return false; | ||
} | ||
|
||
let Some(ast::Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ })) = | ||
arguments.find_argument("sep", 0) | ||
else { | ||
return false; | ||
}; | ||
|
||
if value.to_str() != "." { | ||
return false; | ||
} | ||
|
||
is_dunder_version_split(func) | ||
} | ||
|
||
fn is_dunder_version_split(func: &ast::Expr) -> bool { | ||
// foo.__version__.split(".") | ||
// ---- value ---- ^^^^^ attr | ||
let ast::Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else { | ||
return false; | ||
}; | ||
if attr != "split" { | ||
return false; | ||
} | ||
is_dunder_version(value) | ||
} | ||
|
||
fn is_dunder_version(expr: &ast::Expr) -> bool { | ||
if let ast::Expr::Name(ast::ExprName { id, .. }) = expr { | ||
return id == "__version__"; | ||
} | ||
|
||
// foo.__version__.split(".") | ||
// ^^^^^^^^^^^ attr | ||
let ast::Expr::Attribute(ast::ExprAttribute { attr, .. }) = expr else { | ||
return false; | ||
}; | ||
|
||
attr == "__version__" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
.../src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF048_RUF048.py.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/ruff/mod.rs | ||
--- | ||
RUF048.py:4:7: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
4 | tuple(map(int, __version__.split("."))) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
5 | list(map(int, __version__.split("."))) | ||
| | ||
|
||
RUF048.py:5:6: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
4 | tuple(map(int, __version__.split("."))) | ||
5 | list(map(int, __version__.split("."))) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
6 | | ||
7 | # `sep` passed as keyword argument | ||
| | ||
|
||
RUF048.py:8:13: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
7 | # `sep` passed as keyword argument | ||
8 | for part in map(int, __version__.split(sep=".")): | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
9 | print(part) | ||
| |
55 changes: 55 additions & 0 deletions
55
...rc/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF048_RUF048_1.py.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/ruff/mod.rs | ||
--- | ||
RUF048_1.py:5:7: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
5 | tuple(map(int, __version__.split("."))) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
6 | list(map(int, __version__.split("."))) | ||
7 | tuple(map(int, bar.__version__.split("."))) | ||
| | ||
|
||
RUF048_1.py:6:6: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
5 | tuple(map(int, __version__.split("."))) | ||
6 | list(map(int, __version__.split("."))) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
7 | tuple(map(int, bar.__version__.split("."))) | ||
8 | list(map(int, bar.__version__.split("."))) | ||
| | ||
|
||
RUF048_1.py:7:7: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
5 | tuple(map(int, __version__.split("."))) | ||
6 | list(map(int, __version__.split("."))) | ||
7 | tuple(map(int, bar.__version__.split("."))) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
8 | list(map(int, bar.__version__.split("."))) | ||
| | ||
|
||
RUF048_1.py:8:6: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
6 | list(map(int, __version__.split("."))) | ||
7 | tuple(map(int, bar.__version__.split("."))) | ||
8 | list(map(int, bar.__version__.split("."))) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
9 | | ||
10 | # `sep` passed as keyword argument | ||
| | ||
|
||
RUF048_1.py:11:13: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
10 | # `sep` passed as keyword argument | ||
11 | for part in map(int, bar.__version__.split(sep=".")): | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
12 | print(part) | ||
| | ||
|
||
RUF048_1.py:14:13: RUF048 `__version__` may contain non-integral-like elements | ||
| | ||
12 | print(part) | ||
13 | | ||
14 | for part in map(int, __version__.split(sep=".")): | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF048 | ||
15 | print(part) | ||
| |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.