mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +00:00
[flake8-datetimez
] Usages of datetime.max
/datetime.min
(DTZ901
) (#14288)
## Summary Resolves #13217. ## Test Plan `cargo nextest run` and `cargo insta test`. --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
bd30701980
commit
5c548dcc04
9 changed files with 249 additions and 3 deletions
30
crates/ruff_linter/resources/test/fixtures/flake8_datetimez/DTZ901.py
vendored
Normal file
30
crates/ruff_linter/resources/test/fixtures/flake8_datetimez/DTZ901.py
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# Error
|
||||||
|
datetime.datetime.max
|
||||||
|
datetime.datetime.min
|
||||||
|
|
||||||
|
datetime.datetime.max.replace(year=...)
|
||||||
|
datetime.datetime.min.replace(hour=...)
|
||||||
|
|
||||||
|
|
||||||
|
# No error
|
||||||
|
datetime.datetime.max.replace(tzinfo=...)
|
||||||
|
datetime.datetime.min.replace(tzinfo=...)
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# Error
|
||||||
|
datetime.max
|
||||||
|
datetime.min
|
||||||
|
|
||||||
|
datetime.max.replace(year=...)
|
||||||
|
datetime.min.replace(hour=...)
|
||||||
|
|
||||||
|
|
||||||
|
# No error
|
||||||
|
datetime.max.replace(tzinfo=...)
|
||||||
|
datetime.min.replace(tzinfo=...)
|
|
@ -339,6 +339,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::SixPY3) {
|
if checker.enabled(Rule::SixPY3) {
|
||||||
flake8_2020::rules::name_or_attribute(checker, expr);
|
flake8_2020::rules::name_or_attribute(checker, expr);
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::DatetimeMinMax) {
|
||||||
|
flake8_datetimez::rules::datetime_max_min(checker, expr);
|
||||||
|
}
|
||||||
if checker.enabled(Rule::BannedApi) {
|
if checker.enabled(Rule::BannedApi) {
|
||||||
flake8_tidy_imports::rules::banned_attribute_access(checker, expr);
|
flake8_tidy_imports::rules::banned_attribute_access(checker, expr);
|
||||||
}
|
}
|
||||||
|
|
|
@ -706,6 +706,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
(Flake8Datetimez, "007") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeStrptimeWithoutZone),
|
(Flake8Datetimez, "007") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeStrptimeWithoutZone),
|
||||||
(Flake8Datetimez, "011") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDateToday),
|
(Flake8Datetimez, "011") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDateToday),
|
||||||
(Flake8Datetimez, "012") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDateFromtimestamp),
|
(Flake8Datetimez, "012") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDateFromtimestamp),
|
||||||
|
(Flake8Datetimez, "901") => (RuleGroup::Preview, rules::flake8_datetimez::rules::DatetimeMinMax),
|
||||||
|
|
||||||
// pygrep-hooks
|
// pygrep-hooks
|
||||||
(PygrepHooks, "001") => (RuleGroup::Removed, rules::pygrep_hooks::rules::Eval),
|
(PygrepHooks, "001") => (RuleGroup::Removed, rules::pygrep_hooks::rules::Eval),
|
||||||
|
|
|
@ -9,6 +9,7 @@ mod tests {
|
||||||
use test_case::test_case;
|
use test_case::test_case;
|
||||||
|
|
||||||
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};
|
||||||
|
|
||||||
|
@ -30,4 +31,18 @@ mod tests {
|
||||||
assert_messages!(snapshot, diagnostics);
|
assert_messages!(snapshot, diagnostics);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(Rule::DatetimeMinMax, Path::new("DTZ901.py"))]
|
||||||
|
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
|
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("flake8_datetimez").join(path).as_path(),
|
||||||
|
&settings::LinterSettings {
|
||||||
|
preview: PreviewMode::Enabled,
|
||||||
|
..settings::LinterSettings::for_rule(rule_code)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_ast::{Expr, ExprAttribute, ExprCall};
|
||||||
|
use ruff_python_semantic::{Modules, SemanticModel};
|
||||||
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for uses of `datetime.datetime.max` and `datetime.datetime.min`.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// `datetime.max` and `datetime.min` are non-timezone-aware datetime objects.
|
||||||
|
///
|
||||||
|
/// As such, operations on `datetime.max` and `datetime.min` may behave
|
||||||
|
/// unexpectedly, as in:
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// # Timezone: UTC-14
|
||||||
|
/// datetime.max.timestamp() # ValueError: year 10000 is out of range
|
||||||
|
/// datetime.min.timestamp() # ValueError: year 0 is out of range
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// datetime.max
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// datetime.max.replace(tzinfo=datetime.UTC)
|
||||||
|
/// ```
|
||||||
|
#[violation]
|
||||||
|
pub struct DatetimeMinMax {
|
||||||
|
min_max: MinMax,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Violation for DatetimeMinMax {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
let DatetimeMinMax { min_max } = self;
|
||||||
|
format!("Use of `datetime.datetime.{min_max}` without timezone information")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_title(&self) -> Option<String> {
|
||||||
|
let DatetimeMinMax { min_max } = self;
|
||||||
|
Some(format!(
|
||||||
|
"Replace with `datetime.datetime.{min_max}.replace(tzinfo=...)`"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DTZ901
|
||||||
|
pub(crate) fn datetime_max_min(checker: &mut Checker, expr: &Expr) {
|
||||||
|
let semantic = checker.semantic();
|
||||||
|
|
||||||
|
if !semantic.seen_module(Modules::DATETIME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let min_max = match qualified_name.segments() {
|
||||||
|
["datetime", "datetime", "min"] => MinMax::Min,
|
||||||
|
["datetime", "datetime", "max"] => MinMax::Max,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if followed_by_replace_tzinfo(checker.semantic()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checker
|
||||||
|
.diagnostics
|
||||||
|
.push(Diagnostic::new(DatetimeMinMax { min_max }, expr.range()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the current expression has the pattern `foo.replace(tzinfo=bar)`.
|
||||||
|
fn followed_by_replace_tzinfo(semantic: &SemanticModel) -> bool {
|
||||||
|
let Some(parent) = semantic.current_expression_parent() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(grandparent) = semantic.current_expression_grandparent() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
match (parent, grandparent) {
|
||||||
|
(Expr::Attribute(ExprAttribute { attr, .. }), Expr::Call(ExprCall { arguments, .. })) => {
|
||||||
|
attr.as_str() == "replace" && arguments.find_keyword("tzinfo").is_some()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
enum MinMax {
|
||||||
|
/// `datetime.datetime.min`
|
||||||
|
Min,
|
||||||
|
/// `datetime.datetime.max`
|
||||||
|
Max,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MinMax {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MinMax::Min => write!(f, "min"),
|
||||||
|
MinMax::Max => write!(f, "max"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,10 +8,10 @@ pub(super) enum DatetimeModuleAntipattern {
|
||||||
NonePassedToTzArgument,
|
NonePassedToTzArgument,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the parent expression is a call to `astimezone`. This assumes that
|
/// Check if the parent expression is a call to `astimezone`.
|
||||||
/// the current expression is a `datetime.datetime` object.
|
/// This assumes that the current expression is a `datetime.datetime` object.
|
||||||
pub(super) fn parent_expr_is_astimezone(checker: &Checker) -> bool {
|
pub(super) fn parent_expr_is_astimezone(checker: &Checker) -> bool {
|
||||||
checker.semantic().current_expression_parent().is_some_and( |parent| {
|
checker.semantic().current_expression_parent().is_some_and(|parent| {
|
||||||
matches!(parent, Expr::Attribute(ExprAttribute { attr, .. }) if attr.as_str() == "astimezone")
|
matches!(parent, Expr::Attribute(ExprAttribute { attr, .. }) if attr.as_str() == "astimezone")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ pub(crate) use call_datetime_today::*;
|
||||||
pub(crate) use call_datetime_utcfromtimestamp::*;
|
pub(crate) use call_datetime_utcfromtimestamp::*;
|
||||||
pub(crate) use call_datetime_utcnow::*;
|
pub(crate) use call_datetime_utcnow::*;
|
||||||
pub(crate) use call_datetime_without_tzinfo::*;
|
pub(crate) use call_datetime_without_tzinfo::*;
|
||||||
|
pub(crate) use datetime_min_max::*;
|
||||||
|
|
||||||
mod call_date_fromtimestamp;
|
mod call_date_fromtimestamp;
|
||||||
mod call_date_today;
|
mod call_date_today;
|
||||||
|
@ -17,4 +18,5 @@ mod call_datetime_today;
|
||||||
mod call_datetime_utcfromtimestamp;
|
mod call_datetime_utcfromtimestamp;
|
||||||
mod call_datetime_utcnow;
|
mod call_datetime_utcnow;
|
||||||
mod call_datetime_without_tzinfo;
|
mod call_datetime_without_tzinfo;
|
||||||
|
mod datetime_min_max;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_datetimez/mod.rs
|
||||||
|
---
|
||||||
|
DTZ901.py:5:1: DTZ901 Use of `datetime.datetime.max` without timezone information
|
||||||
|
|
|
||||||
|
4 | # Error
|
||||||
|
5 | datetime.datetime.max
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^ DTZ901
|
||||||
|
6 | datetime.datetime.min
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.max.replace(tzinfo=...)`
|
||||||
|
|
||||||
|
DTZ901.py:6:1: DTZ901 Use of `datetime.datetime.min` without timezone information
|
||||||
|
|
|
||||||
|
4 | # Error
|
||||||
|
5 | datetime.datetime.max
|
||||||
|
6 | datetime.datetime.min
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^ DTZ901
|
||||||
|
7 |
|
||||||
|
8 | datetime.datetime.max.replace(year=...)
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.min.replace(tzinfo=...)`
|
||||||
|
|
||||||
|
DTZ901.py:8:1: DTZ901 Use of `datetime.datetime.max` without timezone information
|
||||||
|
|
|
||||||
|
6 | datetime.datetime.min
|
||||||
|
7 |
|
||||||
|
8 | datetime.datetime.max.replace(year=...)
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^ DTZ901
|
||||||
|
9 | datetime.datetime.min.replace(hour=...)
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.max.replace(tzinfo=...)`
|
||||||
|
|
||||||
|
DTZ901.py:9:1: DTZ901 Use of `datetime.datetime.min` without timezone information
|
||||||
|
|
|
||||||
|
8 | datetime.datetime.max.replace(year=...)
|
||||||
|
9 | datetime.datetime.min.replace(hour=...)
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^ DTZ901
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.min.replace(tzinfo=...)`
|
||||||
|
|
||||||
|
DTZ901.py:21:1: DTZ901 Use of `datetime.datetime.max` without timezone information
|
||||||
|
|
|
||||||
|
20 | # Error
|
||||||
|
21 | datetime.max
|
||||||
|
| ^^^^^^^^^^^^ DTZ901
|
||||||
|
22 | datetime.min
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.max.replace(tzinfo=...)`
|
||||||
|
|
||||||
|
DTZ901.py:22:1: DTZ901 Use of `datetime.datetime.min` without timezone information
|
||||||
|
|
|
||||||
|
20 | # Error
|
||||||
|
21 | datetime.max
|
||||||
|
22 | datetime.min
|
||||||
|
| ^^^^^^^^^^^^ DTZ901
|
||||||
|
23 |
|
||||||
|
24 | datetime.max.replace(year=...)
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.min.replace(tzinfo=...)`
|
||||||
|
|
||||||
|
DTZ901.py:24:1: DTZ901 Use of `datetime.datetime.max` without timezone information
|
||||||
|
|
|
||||||
|
22 | datetime.min
|
||||||
|
23 |
|
||||||
|
24 | datetime.max.replace(year=...)
|
||||||
|
| ^^^^^^^^^^^^ DTZ901
|
||||||
|
25 | datetime.min.replace(hour=...)
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.max.replace(tzinfo=...)`
|
||||||
|
|
||||||
|
DTZ901.py:25:1: DTZ901 Use of `datetime.datetime.min` without timezone information
|
||||||
|
|
|
||||||
|
24 | datetime.max.replace(year=...)
|
||||||
|
25 | datetime.min.replace(hour=...)
|
||||||
|
| ^^^^^^^^^^^^ DTZ901
|
||||||
|
|
|
||||||
|
= help: Replace with `datetime.datetime.min.replace(tzinfo=...)`
|
3
ruff.schema.json
generated
3
ruff.schema.json
generated
|
@ -3023,6 +3023,9 @@
|
||||||
"DTZ01",
|
"DTZ01",
|
||||||
"DTZ011",
|
"DTZ011",
|
||||||
"DTZ012",
|
"DTZ012",
|
||||||
|
"DTZ9",
|
||||||
|
"DTZ90",
|
||||||
|
"DTZ901",
|
||||||
"E",
|
"E",
|
||||||
"E1",
|
"E1",
|
||||||
"E10",
|
"E10",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue