Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ static void Main(string[] args)
}

[TestMethod]
public void UsageOfImplicitConversionInComparison_ProducesWarningMessage()
public void UsageOfImplicitConversionInComparison_DateTimeToDateTimeOffset_ProducesWarningMessage()
{
string source = @"
using System;
Expand All @@ -53,15 +53,40 @@ internal class Program
static void Main(string[] args)
{
DateTime first = DateTime.Now;
DateTimeOffset second = DateTimeOffset.Now;
_ = first < second;
}
}
}";

Thread.Sleep(10);
VerifyCSharpDiagnostic(source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations =
[new DiagnosticResultLocation("Test0.cs", 13, 17)]
});

DateTimeOffset second = DateTimeOffset.Now;
}

if (first < second)
{
Console.WriteLine(""Time has passed..."");
}
[TestMethod]
public void UsageOfImplicitConversionInComparison_DateTimeOffsetToDateTime_ProducesWarningMessage()
{
string source = @"
using System;
using System.Threading;

namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
DateTimeOffset first = DateTimeOffset.Now;
DateTime second = DateTime.Now;
_ = first < second;
}
}
}";
Expand All @@ -73,13 +98,152 @@ static void Main(string[] args)
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations =
[
new DiagnosticResultLocation("Test0.cs", 17, 17)
]
[new DiagnosticResultLocation("Test0.cs", 13, 25)]
});

}

[TestMethod]
public void UsageOfImplicitConversionInComparison_NullableDateTimeOffsetToDateTime_ProducesWarningMessage()
{
string source = @"
using System;
using System.Threading;

namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
DateTimeOffset? first = DateTimeOffset.Now;
DateTime second = DateTime.Now;
_ = first < second;
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 25)]
}
);
}

[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
[TestMethod]
public void UsageOfImplicitConversion_WithProperties_ProducesWarningMessage()
{
string source = @"
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);

internal class Program
{
static void Main(string[] args)
{
Pair pair = new(DateTimeOffset.Now, DateTime.Now);
_ = pair.DateTimeOffset < pair.DateTime;
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 29)]
}
);
}

[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
[TestMethod]
public void UsageOfImplicitConversion_WithPropertiesInLinq_ProducesWarningMessage()
{
string source = @"
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);

internal class Program
{
static void Main(string[] args)
{
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) };
_ = list.Where(pair => pair.DateTimeOffset < pair.DateTime);
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 42)]
}
);
}

[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
[TestMethod]
public void UsageOfImplicitConversion_InLinqWithVariables_ProducesWarningMessage()
{
string source = @"
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);

internal class Program
{
static void Main(string[] args)
{
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) };
_ = list.Where(pair => {
DateTimeOffset first = pair.DateTimeOffset;
DateTime second = pair.DateTime;
return first < second;
});
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 24)]
}
);
}

[TestMethod]
public void UsageOfExplicitConversion_ProducesNothing()
{
Expand All @@ -101,6 +265,52 @@ static void Main(string[] args)

}

[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
[TestMethod]
public void UsageInLambdaWithDateProperty_ProducesWarningMessage()
{
// This test matches the original issue scenario
string source = @"
using System;
using System.Linq;

namespace Test
{
public class TimeEntry
{
public DateTimeOffset EndDate { get; set; }
public DateTimeOffset StartDate { get; set; }
}

public class DataSource
{
public IQueryable<TimeEntry> GetQuery(DateTime startDate, DateTime endDate)
{
return Enumerable.Empty<TimeEntry>().AsQueryable()
.Where(te =>
te.EndDate <= endDate.Date.AddDays(1).AddTicks(-1) &&
te.StartDate >= startDate.Date);
}
}
}";

VerifyCSharpDiagnostic(source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 35)]
},
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 19, 38)]
});
}

protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

Expand Down Expand Up @@ -28,27 +30,88 @@ public override void Initialize(AnalysisContext context)

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Conversion);
context.RegisterOperationAction(AnalyzeConversion, OperationKind.Conversion);
context.RegisterOperationAction(AnalyzeBinaryOperation, OperationKind.Binary);
}

private void AnalyzeInvocation(OperationAnalysisContext context)
private void AnalyzeConversion(OperationAnalysisContext context)
{
if (context.Operation is not IConversionOperation conversionOperation)
if (context.Operation is not IConversionOperation conversionOperation || !conversionOperation.Conversion.IsImplicit)
{
return;
}

if (conversionOperation.Conversion.IsImplicit && conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
ITypeSymbol sourceType = conversionOperation.Operand.Type;
ITypeSymbol targetType = conversionOperation.Type;

if (sourceType is null || targetType is null)
{
return;
}

INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
?? throw new InvalidOperationException("System.DateTime type not found in compilation.");
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
?? throw new InvalidOperationException("System.DateTimeOffset type not found in compilation.");

// Check if source is DateTime and target is DateTimeOffset
if (SymbolEqualityComparer.Default.Equals(sourceType, dateTimeType) &&
SymbolEqualityComparer.Default.Equals(targetType, dateTimeOffsetType))
{
// Report the diagnostic at the operand's location (the DateTime expression being converted)
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Operand.Syntax.GetLocation()));
}
}

private void AnalyzeBinaryOperation(OperationAnalysisContext context)
{
if (context.Operation is not IBinaryOperation binaryOperation)
{
return;
}

INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
?? throw new InvalidOperationException("System.DateTime type not found in compilation.");
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
?? throw new InvalidOperationException("System.DateTimeOffset type not found in compilation.");

// For binary operations, check if either operand is DateTime and the other is DateTimeOffset
// This catches cases where IConversionOperation nodes are not created (e.g., property access)
CheckBinaryOperandPair(context, binaryOperation.LeftOperand, binaryOperation.RightOperand, dateTimeType, dateTimeOffsetType);
CheckBinaryOperandPair(context, binaryOperation.RightOperand, binaryOperation.LeftOperand, dateTimeType, dateTimeOffsetType);
}

private void CheckBinaryOperandPair(OperationAnalysisContext context, IOperation operand, IOperation otherOperand, INamedTypeSymbol dateTimeType, INamedTypeSymbol dateTimeOffsetType)
{
if (operand?.Type is null || otherOperand?.Type is null)
{
return;
}

// Skip if we don't have a syntax location to report
if (operand.Syntax is null)
{
INamedTypeSymbol containingType = conversionOperation.Conversion.MethodSymbol.ContainingType;
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
if (SymbolEqualityComparer.Default.Equals(containingType, dateTimeOffsetType))
{
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
}
return;
}

// Unwrap nullable types if present
ITypeSymbol operandType = operand.Type is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable
? nullable.TypeArguments[0]
: operand.Type;

ITypeSymbol otherType = otherOperand.Type is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } otherNullable
? otherNullable.TypeArguments[0]
: otherOperand.Type;

// Check if operand is DateTime and other operand is DateTimeOffset
bool isDateTimeOperand = SymbolEqualityComparer.Default.Equals(operandType, dateTimeType);
bool isDateTimeOffsetOtherOperand = SymbolEqualityComparer.Default.Equals(otherType, dateTimeOffsetType);

if (isDateTimeOperand && isDateTimeOffsetOtherOperand)
{
// DateTime will be implicitly converted to DateTimeOffset in the comparison
context.ReportDiagnostic(Diagnostic.Create(_Rule202, operand.Syntax.GetLocation()));
}
}

private static class Rule202
Expand Down