Skip to content

Evaluate every component of the TimeSpan factory methods and constructors - #1440

Merged
meziantou merged 1 commit into
mainfrom
feature/timespan-factory-evaluation-06c056
Sep 8, 2026
Merged

meziantou merged 1 commit into
mainfrom
feature/timespan-factory-evaluation-06c056

Conversation

@meziantou

Copy link
Copy Markdown
Owner

What was wrong

TimeSpanOperation recognized the TimeSpan.FromXXX methods by name and read only Arguments[0], and recognized the constructors by the number of their arguments. Every additional component was silently dropped.

new Regex("a", RegexOptions.None, TimeSpan.FromSeconds(1, 500))

The timeout is 1500 ms, but MA0110 recorded RegexTimeout=1000 and the code fixer generated:

[GeneratedRegex("a", RegexOptions.None, matchTimeoutMilliseconds: 1000)]

The fixed code compiles, but a workload that used to fit within the timeout can now time out.

The same helper backs MA0132 and MA0133, where the truncation to milliseconds produced a false positive: new DateTimeOffset(DateTime.UnixEpoch, new TimeSpan(1)) has a one tick offset, which was evaluated as 0 and reported as DateTimeOffset.UnixEpoch.

What changed

TimeSpanOperation now computes ticks and drives everything off parameter identity:

  • The unit of a component comes from the name of its parameter (days, hours, minutes, seconds, milliseconds, microseconds, ticks), so the optional components, the additional components and the named arguments given in any order are all evaluated. The overloads taking a single value name their parameter value, so its unit comes from the method name. This also covers the microsecond constructor and TimeSpan.FromMicroseconds, which were previously unknown.
  • A parameter that does not name a known unit makes the whole expression unknown, so an unsupported overload is unknown instead of being partially evaluated.
  • The arithmetic of the components is checked, so a duration that no TimeSpan can hold, which the code creating it would reject at runtime, is unknown.
  • GetMilliseconds returns a value only when the duration is an exact number of milliseconds, as the callers replace the expression with a number of milliseconds. TimeSpan.MinValue, TimeSpan.MaxValue and the durations shorter than a millisecond are now unknown rather than truncated.

UseRegexSourceGeneratorAnalyzer.IsConstant additionally rejects the timeouts longer than Int32.MaxValue milliseconds. That closes a second silent drop: matchTimeoutMilliseconds is an Int32, and the fixer's TryParseInt32 returned null for a longer value, which removed the TimeSpan argument from the call and omitted the timeout from the generated attribute.

Note for the reviewer

The Timeout.Infinite case of the field lookup is removed. It is a const int, so it can never be a TimeSpan typed operation, and as a component it was folded into the constant path: the case was unreachable. Timeout.InfiniteTimeSpan and Regex.InfiniteMatchTimeout are still evaluated as -1.

MA0110 no longer reports a few Regex constructions it used to report, all of which produced a fix that changed the timeout or dropped it: TimeSpan.MinValue, TimeSpan.MaxValue, the durations shorter than a millisecond and the durations longer than Int32.MaxValue milliseconds. docs/Rules/MA0110.md documents this.

Tests

  • Timeout_MultipleComponents, 8 cases: FromSeconds(1, 500), the named arguments given in another order, FromMilliseconds(1, 2000), FromMinutes(1, 30), FromHours(1, 2, 3, 4), FromDays(1, 2, 3, 4), new TimeSpan(seconds: 3, minutes: 2, hours: 1) and the microsecond constructor. It pins the .NET 9 reference assemblies, which is where those overloads were added.
  • Timeout_NotRepresentableInMilliseconds_NoDiagnostic: new TimeSpan(1), FromMilliseconds(0.5), TimeSpan.MaxValue and FromDays(30).
  • Two cases added to each of the MA0132/MA0133 theories, including the new TimeSpan(1) false positive and FromMinutes(0, 60).

These are regression tests: reverting only TimeSpanOperation while keeping them produces 11 failures.

dotnet test passes for the five Roslyn versions (20689 tests). dotnet run --project src/DocumentationGenerator exits 0 with no further change.

…tors

TimeSpanOperation recognized the TimeSpan.FromXXX methods by name and read
only their first argument, and recognized the constructors by the number of
their arguments, so every additional component was silently dropped:
TimeSpan.FromSeconds(1, 500) evaluated to 1000 ms, and MA0110 generated
[GeneratedRegex(..., matchTimeoutMilliseconds: 1000)] for a 1500 ms timeout.
It also truncated the durations shorter than a millisecond to 0, so MA0132
and MA0133 considered new TimeSpan(1) to be TimeSpan.Zero.

The evaluation now computes ticks and takes the unit of a component from the
name of its parameter, so the optional components, the additional components
and the named arguments given in any order are all evaluated, and an overload
with a parameter that does not name a known unit is unknown instead of being
partially evaluated. The arithmetic is checked, so a duration that no TimeSpan
can hold is unknown too, and a duration that is not an exact number of
milliseconds is unknown as the callers replace it with a number of
milliseconds.

MA0110 also rejects the timeouts longer than Int32.MaxValue milliseconds:
matchTimeoutMilliseconds is an Int32, and the code fixer used to drop both the
TimeSpan argument and the attribute argument when the value did not fit.
@meziantou
meziantou merged commit 142089e into main Sep 8, 2026
13 checks passed
@meziantou
meziantou deleted the feature/timespan-factory-evaluation-06c056 branch September 8, 2026 17:29
This was referenced Sep 8, 2026
This was referenced Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant