Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/Rules/MA0110.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ private static partial Regex MyRegex();
````

The rule is not reported when the `Regex` cannot be expressed with `[GeneratedRegex]`, such as when the match
timeout is neither infinite nor strictly positive:
timeout is neither infinite nor strictly positive, or when it is not an `Int32` number of milliseconds:

````c#
// ok: GeneratedRegex does not accept this match timeout
new Regex("constant pattern", RegexOptions.None, TimeSpan.Zero);

// ok: GeneratedRegex takes an Int32 number of milliseconds, which cannot represent this match timeout
new Regex("constant pattern", RegexOptions.None, TimeSpan.FromMicroseconds(500));
````

229 changes: 131 additions & 98 deletions src/Meziantou.Analyzer/Internals/TimeSpanOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ namespace Meziantou.Analyzer.Internals;

internal sealed class TimeSpanOperation(Compilation compilation)
{
private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;

private readonly ISymbol? _timeSpanSymbol = compilation.GetBestTypeByMetadataName("System.TimeSpan");
private readonly ISymbol? _regexSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex");
private readonly ISymbol? _timeoutSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Timeout");

/// <summary>
/// Gets the duration of a <see cref="TimeSpan"/> expression, or <see langword="null"/> when the expression is not
/// supported or when its duration is not an exact number of milliseconds.
/// </summary>
public long? GetMilliseconds(IOperation op)
{
if (op.SemanticModel is null)
Expand All @@ -14,134 +20,161 @@ internal sealed class TimeSpanOperation(Compilation compilation)
if (!op.Type.IsEqualTo(_timeSpanSymbol))
return null;

return GetMilliseconds(op, 1d);
}

private long? GetMilliseconds(IOperation op, double factor)
{
const double TicksToMilliseconds = 1d / TimeSpan.TicksPerMillisecond;
const double SecondsToMilliseconds = 1000;
const double MinutesToMilliseconds = 60 * 1000;
const double HoursToMilliseconds = 60 * 60 * 1000;
const double DaysToMilliseconds = 24 * 60 * 60 * 1000;
long ticks;
try
{
if (GetTicks(op) is not long value)
return null;

op = op.UnwrapImplicitConversions();
if (op.ConstantValue.HasValue)
ticks = value;
}
catch (OverflowException)
{
if (op.ConstantValue.HasValue && op.ConstantValue.Value is long int64Value)
return (long)(int64Value * factor);
// The arithmetic of the components is checked, so a duration that cannot be represented by a
// TimeSpan, which the code creating it would report at runtime, is unknown
return null;
}

if (op.ConstantValue.HasValue && op.ConstantValue.Value is int int32Value)
return (long)(int32Value * factor);
// The callers replace the expression with a number of milliseconds, so a duration that is not an exact
// number of milliseconds cannot be reported without changing the behavior of the code
if (ticks % TimeSpan.TicksPerMillisecond is not 0)
return null;

if (op.ConstantValue.HasValue && op.ConstantValue.Value is double doubleValue)
return (long)(doubleValue * factor);
}
return ticks / TimeSpan.TicksPerMillisecond;
}

private long? GetTicks(IOperation op)
{
op = op.UnwrapImplicitConversions();

if (op is IDefaultValueOperation)
return 0L;

if (op is IFieldReferenceOperation fieldReferenceOperation)
return GetFieldTicks(fieldReferenceOperation.Member);

// TimeSpan.FromSeconds(1, milliseconds: 500)
if (op is IInvocationOperation invocationOperation)
{
var method = invocationOperation.TargetMethod;
if (method.IsStatic && method.ContainingType.IsEqualTo(_timeSpanSymbol))
{
return method.Name switch
{
"FromTicks" => GetMilliseconds(invocationOperation.Arguments[0].Value, TicksToMilliseconds),
"FromMilliseconds" => GetMilliseconds(invocationOperation.Arguments[0].Value, 1),
"FromSeconds" => GetMilliseconds(invocationOperation.Arguments[0].Value, SecondsToMilliseconds),
"FromMinutes" => GetMilliseconds(invocationOperation.Arguments[0].Value, MinutesToMilliseconds),
"FromHours" => GetMilliseconds(invocationOperation.Arguments[0].Value, HoursToMilliseconds),
"FromDays" => GetMilliseconds(invocationOperation.Arguments[0].Value, DaysToMilliseconds),
_ => null,
};
}
if (!method.IsStatic || !method.ContainingType.IsEqualTo(_timeSpanSymbol))
return null;

return null;
return GetComponentsTicks(invocationOperation.Arguments, GetTicksPerUnit(method.Name));
}

if (op is IFieldReferenceOperation fieldReferenceOperation)
{
var member = fieldReferenceOperation.Member;
if (member.IsStatic && member.ContainingType.IsEqualTo(_timeSpanSymbol))
{
return member.Name switch
{
"Zero" => 0,
"MinValue" => (long)TimeSpan.MinValue.TotalMilliseconds,
"MaxValue" => (long)TimeSpan.MaxValue.TotalMilliseconds,
_ => null,
};
}
// new TimeSpan(hours: 1, minutes: 2, seconds: 3)
if (op is IObjectCreationOperation objectCreationOperation && objectCreationOperation.Type.IsEqualTo(_timeSpanSymbol))
return GetComponentsTicks(objectCreationOperation.Arguments, valueParameterTicksPerUnit: null);

if (member.IsStatic && member.ContainingType.IsEqualTo(_regexSymbol))
{
return member.Name switch
{
"InfiniteMatchTimeout" => -1L,
_ => null,
};
}
return null;
}

private long? GetFieldTicks(ISymbol member)
{
if (!member.IsStatic)
return null;

if (member.IsStatic && member.ContainingType.IsEqualTo(_timeoutSymbol))
if (member.ContainingType.IsEqualTo(_timeSpanSymbol))
{
return member.Name switch
{
return member.Name switch
{
"InfiniteTimeSpan" => -1L,
"Infinite" => -1L,
_ => null,
};
}
"Zero" => 0L,
"MinValue" => TimeSpan.MinValue.Ticks,
"MaxValue" => TimeSpan.MaxValue.Ticks,
_ => null,
};
}

return null;
if (member.ContainingType.IsEqualTo(_regexSymbol))
{
return member.Name switch
{
"InfiniteMatchTimeout" => -TimeSpan.TicksPerMillisecond,
_ => null,
};
}

if (op is IObjectCreationOperation objectCreationOperation)
if (member.ContainingType.IsEqualTo(_timeoutSymbol))
{
if (objectCreationOperation.Type.IsEqualTo(_timeSpanSymbol))
return member.Name switch
{
return objectCreationOperation.Arguments.Length switch
{
// new TimeSpan(long ticks)
1 => GetMilliseconds(objectCreationOperation.Arguments[0].Value, 1d / TimeSpan.TicksPerMillisecond),

// new TimeSpan(int hours, int minutes, int seconds)
3 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, HoursToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[1].Value, MinutesToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[2].Value, SecondsToMilliseconds)),

// new TimeSpan(int days, int hours, int minutes, int seconds)
4 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds)),

// new TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds)
5 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds),
GetMilliseconds(objectCreationOperation.Arguments[4].Value, 1)),
_ => null,
};
}
"InfiniteTimeSpan" => -TimeSpan.TicksPerMillisecond,
_ => null,
};
}

return null;
}

static long? AddValues(params ReadOnlySpan<long?> values)
/// <summary>
/// Sums the components of a <c>TimeSpan.FromXXX</c> overload or of a <see cref="TimeSpan"/> constructor. The unit
/// of a component comes from the name of its parameter, so the optional components and the named arguments given
/// in any order are evaluated too, and an overload with an unknown component is unknown.
/// </summary>
/// <param name="valueParameterTicksPerUnit">
/// The unit of the <c>value</c> parameter of the overloads taking a single value, such as
/// <see cref="TimeSpan.FromSeconds(double)"/>, which is named after the method instead of the unit.
/// </param>
private static long? GetComponentsTicks(ImmutableArray<IArgumentOperation> arguments, long? valueParameterTicksPerUnit)
{
var ticks = 0L;
foreach (var argument in arguments)
{
var result = 0L;
foreach (var value in values)
{
if (!value.HasValue)
return null;
var parameter = argument.Parameter;
if (parameter is null)
return null;

result += value.GetValueOrDefault();
var ticksPerUnit = parameter.Name is "value" ? valueParameterTicksPerUnit : GetTicksPerUnit(parameter.Name);
if (ticksPerUnit is null)
return null;

if (GetComponentTicks(argument.Value, ticksPerUnit.GetValueOrDefault()) is not long componentTicks)
return null;

checked
{
ticks += componentTicks;
}
}

return result;
return ticks;
}

private static long? GetComponentTicks(IOperation operation, long ticksPerUnit)
{
var constantValue = operation.ConstantValue;
if (!constantValue.HasValue)
return null;

checked
{
return constantValue.Value switch
{
int int32Value => int32Value * ticksPerUnit,
long int64Value => int64Value * ticksPerUnit,

// The overloads taking a double multiply the value by the number of ticks of the unit and truncate the
// result. The conversion is checked, so a value that is out of range or NaN is unknown.
double doubleValue => (long)(doubleValue * ticksPerUnit),
_ => null,
};
}
}

/// <summary>
/// Gets the number of ticks of a unit named by the parameter of a <see cref="TimeSpan"/> component, such as
/// <c>seconds</c>, or by a <c>TimeSpan.FromXXX</c> method, such as <c>FromSeconds</c>.
/// </summary>
private static long? GetTicksPerUnit(string name) => name switch
{
"ticks" or "FromTicks" => 1L,
"microseconds" or "FromMicroseconds" => TicksPerMicrosecond,
"milliseconds" or "FromMilliseconds" => TimeSpan.TicksPerMillisecond,
"seconds" or "FromSeconds" => TimeSpan.TicksPerSecond,
"minutes" or "FromMinutes" => TimeSpan.TicksPerMinute,
"hours" or "FromHours" => TimeSpan.TicksPerHour,
"days" or "FromDays" => TimeSpan.TicksPerDay,
_ => null,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,11 @@ private bool IsConstant(IArgumentOperation argumentOperation)
if (valueOperation.Type.IsEqualTo(_timespanSymbol))
{
// GeneratedRegex only accepts an infinite or strictly positive match timeout, so a Regex built with
// any other value cannot be converted: the source generator would reject the generated attribute
// any other value cannot be converted: the source generator would reject the generated attribute.
// Its timeout is an Int32 number of milliseconds, so a longer duration cannot be converted either.
const long Infinite = -1;
var milliseconds = _timeSpanOperation.GetMilliseconds(valueOperation);
return milliseconds is Infinite || milliseconds > 0;
return milliseconds is Infinite or (> 0 and <= int.MaxValue);
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ void Test()
[InlineData("new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, 0, TimeSpan.Zero)")]
[InlineData("new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, 0, default(TimeSpan))")]
[InlineData("new DateTimeOffset(offset: TimeSpan.Zero, day: 1, month: 1, year: 1970, hour: 0, minute: 0, second: 0)")]
[InlineData("new DateTimeOffset(DateTime.UnixEpoch, TimeSpan.FromMinutes(0, 0))")]
[InlineData("new DateTimeOffset(DateTime.UnixEpoch, new TimeSpan(0, 0, 0, 0, 0, 0))")]
public Task UnixEpoch_DateTimeOffset(string code)
{
var test = CreateTest();
Expand Down Expand Up @@ -115,6 +117,8 @@ void Test()
[InlineData("new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, 0, TimeSpan.FromHours(-1))")]
[InlineData("new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, null, TimeSpan.Zero)")]
[InlineData("new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, new System.Globalization.GregorianCalendar(), TimeSpan.Zero)")]
[InlineData("new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.FromMinutes(0, 60))")]
[InlineData("new DateTimeOffset(DateTime.UnixEpoch, new TimeSpan(1))")]
public Task NonUnixEpoch_DateTimeOffset(string code)
{
var test = CreateTest();
Expand Down
Loading