avoid_dst_unsafe_date_arithmetic
v1.1.0 Warning Fix Code Quality
This rule flags add and subtract calls that shift a local DateTime by a whole number of days using a Duration — d.add(const Duration(days: 1)), and the same duration reached through a name.
Duration measures absolute elapsed time. A calendar day is not always 24 hours long: in any zone that observes daylight saving time, one day each year is 23 hours and another is 25. In Europe/Warsaw:
DateTime(2025, 3, 30).add(const Duration(days: 1));// 2025-03-31 01:00 — midnight is no longer midnight“Start of day” is exactly what date arithmetic usually computes, so a boundary that drifts to 01:00 puts events in the wrong bucket and makes a “7 days ago” filter cover 6 days and 23 hours. The bug reproduces on two days of the year and never in UTC, so a CI machine running in UTC is guaranteed to stay green.
This rule is in the recommended preset and takes no configuration. It ships a quick fix.
See also: DateTime.add API docs, which carries this exact caveat.
// Shifts by 24 absolute hours, not by a calendar day.DateTime nextDue(DateTime today) => today.add(const Duration(days: 1));Bump the day component. The DateTime constructor normalises out-of-range
values against the real calendar, so the wall-clock time survives a transition —
and month and year rollover work for free:
DateTime nextDue(DateTime today) => DateTime( today.year, today.month, today.day + 1, today.hour, today.minute,);The quick fix writes exactly this.
More examples
Section titled “More examples”A window that is an hour short twice a year
Section titled “A window that is an hour short twice a year”// Don'tDateTime windowStart(DateTime now) => now.subtract(const Duration(days: 7));// DoDateTime windowStart(DateTime now) => DateTime( now.year, now.month, now.day - 7, now.hour, now.minute,);Whole multiples of 24 hours count too
Section titled “Whole multiples of 24 hours count too”Spelling a day as hours does not change the semantics, so it is reported the same way:
// Don't — 48 hours is two calendar days written the long way.DateTime inTwoDays(DateTime today) => today.add(const Duration(hours: 48));// DoDateTime inTwoDays(DateTime today) => DateTime( today.year, today.month, today.day + 2, today.hour, today.minute,);A named duration is not a defence
Section titled “A named duration is not a defence”This is the shape the defect actually survives review in — the call site reads
like domain language and the Duration is somewhere else:
enum LeadTime { oneMonthBefore;
Duration get offsetFromEvent => const Duration(days: 30);}
// Don't — reported: the getter is resolved back to its declaration.DateTime reminderFor(DateTime occurrence, LeadTime leadTime) => occurrence.subtract(leadTime.offsetFromEvent);// Do — work in UTC, which has no transitions.DateTime reminderFor(DateTime occurrence) => occurrence.toUtc().subtract(const Duration(days: 30));Sub-day durations are correct as they are
Section titled “Sub-day durations are correct as they are”Duration(hours: 2) genuinely means two elapsed hours, and absolute arithmetic
is the right semantics for it:
// Not reported.DateTime expiry(DateTime issuedAt) => issuedAt.add(const Duration(hours: 2));
// Not reported either — a sub-day component day arithmetic cannot reproduce.DateTime odd(DateTime d) => d.add(const Duration(days: 1, hours: 2));UTC receivers are skipped
Section titled “UTC receivers are skipped”// Not reported — statically evident as UTC.DateTime tomorrowUtc() => DateTime.utc(2025, 3, 30).add(const Duration(days: 1));
DateTime shift(DateTime d) => d.toUtc().add(const Duration(days: 1));For anything more involved than shifting a day, use
package:timezone, which models real zone
rules.
Known limitations
Section titled “Known limitations”A named duration resolves only within the library under analysis when it is a getter. A constant is evaluated, which works across package boundaries; a getter is code, so its body is read from the AST and a getter declared in a dependency stays silent rather than being guessed at. The same applies to a parameter or a value returned by a function.
UTC detection is syntactic. DateTime.utc(...), .toUtc(), a chain of shifts on either, and a local variable initialised from one are skipped. Anything less direct — a field, a parameter, a function’s return value — is reported even when it holds a UTC DateTime at runtime. Silence those with // ignore: many_lints/avoid_dst_unsafe_date_arithmetic.
The quick fix is narrower than the rule. It applies only when the receiver is a simple identifier and the shift is a plain integer literal, because the replacement reads the receiver eight times — duplicating a call like loadDate() would change behaviour, not just semantics. The rule still reports; you write the replacement.
Configuration
Section titled “Configuration”This rule is in the recommended preset, so it is on with preset: recommended
or preset: opinionated.
To turn it off:
rules: avoid_dst_unsafe_date_arithmetic: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_accessing_other_classes_private_members— Make the underscore mean what everyone reads it as.avoid_commented_out_code— Detect and flag commented-out code.avoid_complex_conditions— Keep boolean conditions within an operand budget.avoid_deep_nesting— Keep control flow within a nesting budget.