Skip to content

Commit 8132a96

Browse files
Add binary operation handler for DateTime to DateTimeOffset conversion detection (WIP)
Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com>
1 parent 62f434c commit 8132a96

2 files changed

Lines changed: 107 additions & 16 deletions

File tree

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,51 @@ static void Main(string[] args)
262262

263263
}
264264

265+
[TestMethod]
266+
public void UsageInLambdaWithDateProperty_ProducesWarningMessage()
267+
{
268+
// This test matches the original issue scenario
269+
string source = @"
270+
using System;
271+
using System.Linq;
272+
273+
namespace Test
274+
{
275+
public class TimeEntry
276+
{
277+
public DateTimeOffset EndDate { get; set; }
278+
public DateTimeOffset StartDate { get; set; }
279+
}
280+
281+
public class DataSource
282+
{
283+
public IQueryable<TimeEntry> GetQuery(DateTime startDate, DateTime endDate)
284+
{
285+
return Enumerable.Empty<TimeEntry>().AsQueryable()
286+
.Where(te =>
287+
te.EndDate <= endDate.Date.AddDays(1).AddTicks(-1) &&
288+
te.StartDate >= startDate.Date);
289+
}
290+
}
291+
}";
292+
293+
VerifyCSharpDiagnostic(source,
294+
new DiagnosticResult
295+
{
296+
Id = "INTL0202",
297+
Severity = DiagnosticSeverity.Warning,
298+
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
299+
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 35)]
300+
},
301+
new DiagnosticResult
302+
{
303+
Id = "INTL0202",
304+
Severity = DiagnosticSeverity.Warning,
305+
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
306+
Locations = [new DiagnosticResultLocation("Test0.cs", 19, 38)]
307+
});
308+
}
309+
265310
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
266311
{
267312
return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion();

IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Immutable;
33
using System.Linq;
44
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
56
using Microsoft.CodeAnalysis.Diagnostics;
67
using Microsoft.CodeAnalysis.Operations;
78

@@ -30,6 +31,7 @@ public override void Initialize(AnalysisContext context)
3031
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
3132
context.EnableConcurrentExecution();
3233
context.RegisterOperationAction(AnalyzeConversion, OperationKind.Conversion);
34+
context.RegisterOperationAction(AnalyzeBinaryOperation, OperationKind.Binary);
3335
}
3436

3537
private void AnalyzeConversion(OperationAnalysisContext context)
@@ -39,30 +41,74 @@ private void AnalyzeConversion(OperationAnalysisContext context)
3941
return;
4042
}
4143

42-
if (conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
44+
ITypeSymbol sourceType = conversionOperation.Operand.Type;
45+
ITypeSymbol targetType = conversionOperation.Type;
46+
47+
if (sourceType is null || targetType is null)
48+
{
49+
return;
50+
}
51+
52+
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime");
53+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
54+
55+
if (dateTimeType is null || dateTimeOffsetType is null)
4356
{
44-
INamedTypeSymbol containingType = conversionOperation.Conversion.MethodSymbol.ContainingType;
45-
if (IsDateTimeOffsetSymbol(context, containingType))
46-
{
47-
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
48-
}
57+
return;
4958
}
50-
else
59+
60+
// Check if source is DateTime and target is DateTimeOffset
61+
if (SymbolEqualityComparer.Default.Equals(sourceType, dateTimeType) &&
62+
SymbolEqualityComparer.Default.Equals(targetType, dateTimeOffsetType))
5163
{
52-
IOperation implicitDateTimeOffsetOp = conversionOperation.Operand.ChildOperations
53-
.Where(op => op.Kind == OperationKind.Argument && IsDateTimeOffsetSymbol(context, ((IArgumentOperation)op).Value.Type))
54-
.FirstOrDefault();
55-
if (implicitDateTimeOffsetOp != default)
56-
{
57-
context.ReportDiagnostic(Diagnostic.Create(_Rule202, implicitDateTimeOffsetOp.Syntax.GetLocation()));
58-
}
64+
// Report the diagnostic at the operand's location (the DateTime expression being converted)
65+
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Operand.Syntax.GetLocation()));
5966
}
6067
}
6168

62-
private static bool IsDateTimeOffsetSymbol(OperationAnalysisContext context, ITypeSymbol symbol)
69+
private void AnalyzeBinaryOperation(OperationAnalysisContext context)
6370
{
71+
if (context.Operation is not IBinaryOperation binaryOperation)
72+
{
73+
return;
74+
}
75+
76+
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime");
6477
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
65-
return SymbolEqualityComparer.Default.Equals(symbol, dateTimeOffsetType);
78+
79+
if (dateTimeType is null || dateTimeOffsetType is null)
80+
{
81+
return;
82+
}
83+
84+
// For binary operations, check if either operand is DateTime and the other is DateTimeOffset
85+
// This catches cases where IConversionOperation nodes are not created (e.g., property access)
86+
CheckBinaryOperandPair(context, binaryOperation.LeftOperand, binaryOperation.RightOperand, dateTimeType, dateTimeOffsetType);
87+
CheckBinaryOperandPair(context, binaryOperation.RightOperand, binaryOperation.LeftOperand, dateTimeType, dateTimeOffsetType);
88+
}
89+
90+
private void CheckBinaryOperandPair(OperationAnalysisContext context, IOperation operand, IOperation otherOperand, INamedTypeSymbol dateTimeType, INamedTypeSymbol dateTimeOffsetType)
91+
{
92+
if (operand is null || operand.Type is null || otherOperand is null || otherOperand.Type is null)
93+
{
94+
return;
95+
}
96+
97+
// Skip if we don't have a syntax location to report
98+
if (operand.Syntax is null)
99+
{
100+
return;
101+
}
102+
103+
// Check if operand is DateTime and other operand is DateTimeOffset
104+
bool isDateTimeOperand = SymbolEqualityComparer.Default.Equals(operand.Type, dateTimeType);
105+
bool isDateTimeOffsetOtherOperand = SymbolEqualityComparer.Default.Equals(otherOperand.Type, dateTimeOffsetType);
106+
107+
if (isDateTimeOperand && isDateTimeOffsetOtherOperand)
108+
{
109+
// DateTime will be implicitly converted to DateTimeOffset in the comparison
110+
context.ReportDiagnostic(Diagnostic.Create(_Rule202, operand.Syntax.GetLocation()));
111+
}
66112
}
67113

68114
private static class Rule202

0 commit comments

Comments
 (0)