Skip to content

Commit f5fc2a4

Browse files
Rebase INTL0202 binary operation detection onto main
- Rename AnalyzeInvocation -> AnalyzeConversion with improved type-based detection - Add nullable type unwrapping for DateTime?/DateTimeOffset? conversion detection - Add AnalyzeBinaryOperation + CheckBinaryOperandPair for binary expression handling - Keep AnalyzeObjectCreation from main (new DateTimeOffset(DateTime) detection) - Update comparison test to simplified form and add DateTimeOffset->DateTime tests - Add [Ignore] tests for property-based scenarios (not yet implemented)
1 parent a40ba88 commit f5fc2a4

File tree

2 files changed

+295
-22
lines changed

2 files changed

+295
-22
lines changed

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

Lines changed: 220 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ static void Main(string[] args)
4040
}
4141

4242
[TestMethod]
43-
public void UsageOfImplicitConversionInComparison_ProducesWarningMessage()
43+
public void UsageOfImplicitConversionInComparison_DateTimeToDateTimeOffset_ProducesWarningMessage()
4444
{
4545
string source = @"
4646
using System;
@@ -53,15 +53,40 @@ internal class Program
5353
static void Main(string[] args)
5454
{
5555
DateTime first = DateTime.Now;
56+
DateTimeOffset second = DateTimeOffset.Now;
57+
_ = first < second;
58+
}
59+
}
60+
}";
5661

57-
Thread.Sleep(10);
62+
VerifyCSharpDiagnostic(source,
63+
new DiagnosticResult
64+
{
65+
Id = "INTL0202",
66+
Severity = DiagnosticSeverity.Warning,
67+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
68+
Locations =
69+
[new DiagnosticResultLocation("Test0.cs", 13, 17)]
70+
});
5871

59-
DateTimeOffset second = DateTimeOffset.Now;
72+
}
6073

61-
if (first < second)
62-
{
63-
Console.WriteLine(""Time has passed..."");
64-
}
74+
[TestMethod]
75+
public void UsageOfImplicitConversionInComparison_DateTimeOffsetToDateTime_ProducesWarningMessage()
76+
{
77+
string source = @"
78+
using System;
79+
using System.Threading;
80+
81+
namespace ConsoleApp1
82+
{
83+
internal class Program
84+
{
85+
static void Main(string[] args)
86+
{
87+
DateTimeOffset first = DateTimeOffset.Now;
88+
DateTime second = DateTime.Now;
89+
_ = first < second;
6590
}
6691
}
6792
}";
@@ -73,13 +98,152 @@ static void Main(string[] args)
7398
Severity = DiagnosticSeverity.Warning,
7499
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
75100
Locations =
76-
[
77-
new DiagnosticResultLocation("Test0.cs", 17, 17)
78-
]
101+
[new DiagnosticResultLocation("Test0.cs", 13, 25)]
79102
});
80103

81104
}
82105

106+
[TestMethod]
107+
public void UsageOfImplicitConversionInComparison_NullableDateTimeOffsetToDateTime_ProducesWarningMessage()
108+
{
109+
string source = @"
110+
using System;
111+
using System.Threading;
112+
113+
namespace ConsoleApp1
114+
{
115+
internal class Program
116+
{
117+
static void Main(string[] args)
118+
{
119+
DateTimeOffset? first = DateTimeOffset.Now;
120+
DateTime second = DateTime.Now;
121+
_ = first < second;
122+
}
123+
}
124+
}";
125+
126+
VerifyCSharpDiagnostic(
127+
source,
128+
new DiagnosticResult
129+
{
130+
Id = "INTL0202",
131+
Severity = DiagnosticSeverity.Warning,
132+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
133+
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 25)]
134+
}
135+
);
136+
}
137+
138+
[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
139+
[TestMethod]
140+
public void UsageOfImplicitConversion_WithProperties_ProducesWarningMessage()
141+
{
142+
string source = @"
143+
using System;
144+
using System.Collections.Generic;
145+
using System.Linq;
146+
147+
namespace ConsoleApp1
148+
{
149+
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);
150+
151+
internal class Program
152+
{
153+
static void Main(string[] args)
154+
{
155+
Pair pair = new(DateTimeOffset.Now, DateTime.Now);
156+
_ = pair.DateTimeOffset < pair.DateTime;
157+
}
158+
}
159+
}";
160+
161+
VerifyCSharpDiagnostic(
162+
source,
163+
new DiagnosticResult
164+
{
165+
Id = "INTL0202",
166+
Severity = DiagnosticSeverity.Warning,
167+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
168+
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 29)]
169+
}
170+
);
171+
}
172+
173+
[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
174+
[TestMethod]
175+
public void UsageOfImplicitConversion_WithPropertiesInLinq_ProducesWarningMessage()
176+
{
177+
string source = @"
178+
using System;
179+
using System.Collections.Generic;
180+
using System.Linq;
181+
182+
namespace ConsoleApp1
183+
{
184+
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);
185+
186+
internal class Program
187+
{
188+
static void Main(string[] args)
189+
{
190+
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) };
191+
_ = list.Where(pair => pair.DateTimeOffset < pair.DateTime);
192+
}
193+
}
194+
}";
195+
196+
VerifyCSharpDiagnostic(
197+
source,
198+
new DiagnosticResult
199+
{
200+
Id = "INTL0202",
201+
Severity = DiagnosticSeverity.Warning,
202+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
203+
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 42)]
204+
}
205+
);
206+
}
207+
208+
[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
209+
[TestMethod]
210+
public void UsageOfImplicitConversion_InLinqWithVariables_ProducesWarningMessage()
211+
{
212+
string source = @"
213+
using System;
214+
using System.Collections.Generic;
215+
using System.Linq;
216+
217+
namespace ConsoleApp1
218+
{
219+
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);
220+
221+
internal class Program
222+
{
223+
static void Main(string[] args)
224+
{
225+
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) };
226+
_ = list.Where(pair => {
227+
DateTimeOffset first = pair.DateTimeOffset;
228+
DateTime second = pair.DateTime;
229+
return first < second;
230+
});
231+
}
232+
}
233+
}";
234+
235+
VerifyCSharpDiagnostic(
236+
source,
237+
new DiagnosticResult
238+
{
239+
Id = "INTL0202",
240+
Severity = DiagnosticSeverity.Warning,
241+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
242+
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 24)]
243+
}
244+
);
245+
}
246+
83247
[TestMethod]
84248
public void UsageOfExplicitConversion_ProducesNothing()
85249
{
@@ -101,6 +265,52 @@ static void Main(string[] args)
101265

102266
}
103267

268+
[Ignore("Property-based conversion detection not yet implemented - requires Roslyn IOperation tree investigation")]
269+
[TestMethod]
270+
public void UsageInLambdaWithDateProperty_ProducesWarningMessage()
271+
{
272+
// This test matches the original issue scenario
273+
string source = @"
274+
using System;
275+
using System.Linq;
276+
277+
namespace Test
278+
{
279+
public class TimeEntry
280+
{
281+
public DateTimeOffset EndDate { get; set; }
282+
public DateTimeOffset StartDate { get; set; }
283+
}
284+
285+
public class DataSource
286+
{
287+
public IQueryable<TimeEntry> GetQuery(DateTime startDate, DateTime endDate)
288+
{
289+
return Enumerable.Empty<TimeEntry>().AsQueryable()
290+
.Where(te =>
291+
te.EndDate <= endDate.Date.AddDays(1).AddTicks(-1) &&
292+
te.StartDate >= startDate.Date);
293+
}
294+
}
295+
}";
296+
297+
VerifyCSharpDiagnostic(source,
298+
new DiagnosticResult
299+
{
300+
Id = "INTL0202",
301+
Severity = DiagnosticSeverity.Warning,
302+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
303+
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 35)]
304+
},
305+
new DiagnosticResult
306+
{
307+
Id = "INTL0202",
308+
Severity = DiagnosticSeverity.Warning,
309+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
310+
Locations = [new DiagnosticResultLocation("Test0.cs", 19, 38)]
311+
});
312+
}
313+
104314
[TestMethod]
105315
public void UsageOfDateTimeOffsetConstructorWithDateTime_ProducesWarningMessage()
106316
{

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

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,44 @@ public override void Initialize(AnalysisContext context)
2828

2929
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
3030
context.EnableConcurrentExecution();
31-
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Conversion);
31+
context.RegisterOperationAction(AnalyzeConversion, OperationKind.Conversion);
3232
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
33+
context.RegisterOperationAction(AnalyzeBinaryOperation, OperationKind.Binary);
3334
}
3435

35-
private void AnalyzeInvocation(OperationAnalysisContext context)
36+
private void AnalyzeConversion(OperationAnalysisContext context)
3637
{
37-
if (context.Operation is not IConversionOperation conversionOperation)
38+
if (context.Operation is not IConversionOperation conversionOperation || !conversionOperation.Conversion.IsImplicit)
3839
{
3940
return;
4041
}
4142

42-
if (conversionOperation.Conversion.IsImplicit && conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
43+
ITypeSymbol sourceType = conversionOperation.Operand.Type;
44+
ITypeSymbol targetType = conversionOperation.Type;
45+
46+
if (sourceType is null || targetType is null)
4347
{
44-
INamedTypeSymbol containingType = conversionOperation.Conversion.MethodSymbol.ContainingType;
45-
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
46-
if (SymbolEqualityComparer.Default.Equals(containingType, dateTimeOffsetType))
47-
{
48-
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
49-
}
48+
return;
5049
}
5150

51+
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
52+
?? throw new InvalidOperationException("System.DateTime type not found in compilation.");
53+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
54+
?? throw new InvalidOperationException("System.DateTimeOffset type not found in compilation.");
55+
56+
// Unwrap nullable types to handle DateTime? -> DateTimeOffset? conversions
57+
ITypeSymbol unwrappedSource = sourceType is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } srcNullable
58+
? srcNullable.TypeArguments[0]
59+
: sourceType;
60+
ITypeSymbol unwrappedTarget = targetType is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } tgtNullable
61+
? tgtNullable.TypeArguments[0]
62+
: targetType;
5263

64+
if (SymbolEqualityComparer.Default.Equals(unwrappedSource, dateTimeType) &&
65+
SymbolEqualityComparer.Default.Equals(unwrappedTarget, dateTimeOffsetType))
66+
{
67+
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Operand.Syntax.GetLocation()));
68+
}
5369
}
5470

5571
private void AnalyzeObjectCreation(OperationAnalysisContext context)
@@ -81,6 +97,53 @@ private void AnalyzeObjectCreation(OperationAnalysisContext context)
8197
}
8298
}
8399

100+
private void AnalyzeBinaryOperation(OperationAnalysisContext context)
101+
{
102+
if (context.Operation is not IBinaryOperation binaryOperation)
103+
{
104+
return;
105+
}
106+
107+
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
108+
?? throw new InvalidOperationException("System.DateTime type not found in compilation.");
109+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
110+
?? throw new InvalidOperationException("System.DateTimeOffset type not found in compilation.");
111+
112+
CheckBinaryOperandPair(context, binaryOperation.LeftOperand, binaryOperation.RightOperand, dateTimeType, dateTimeOffsetType);
113+
CheckBinaryOperandPair(context, binaryOperation.RightOperand, binaryOperation.LeftOperand, dateTimeType, dateTimeOffsetType);
114+
}
115+
116+
private static void CheckBinaryOperandPair(OperationAnalysisContext context, IOperation operand, IOperation otherOperand, INamedTypeSymbol dateTimeType, INamedTypeSymbol dateTimeOffsetType)
117+
{
118+
if (operand?.Type is null || otherOperand?.Type is null)
119+
{
120+
return;
121+
}
122+
123+
if (operand.Syntax is null)
124+
{
125+
return;
126+
}
127+
128+
// Unwrap nullable types if present
129+
ITypeSymbol operandType = operand.Type is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable
130+
? nullable.TypeArguments[0]
131+
: operand.Type;
132+
133+
ITypeSymbol otherType = otherOperand.Type is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } otherNullable
134+
? otherNullable.TypeArguments[0]
135+
: otherOperand.Type;
136+
137+
bool isDateTimeOperand = SymbolEqualityComparer.Default.Equals(operandType, dateTimeType);
138+
bool isDateTimeOffsetOtherOperand = SymbolEqualityComparer.Default.Equals(otherType, dateTimeOffsetType);
139+
140+
if (isDateTimeOperand && isDateTimeOffsetOtherOperand)
141+
{
142+
// DateTime will be implicitly converted to DateTimeOffset in the comparison
143+
context.ReportDiagnostic(Diagnostic.Create(_Rule202, operand.Syntax.GetLocation()));
144+
}
145+
}
146+
84147
private static class Rule202
85148
{
86149
internal const string DiagnosticId = "INTL0202";
@@ -89,10 +152,10 @@ private static class Rule202
89152
#pragma warning disable INTL0001 // Allow field to not be prefixed with an underscore to match the style
90153
internal static readonly string HelpMessageUri = DiagnosticUrlBuilder.GetUrl(Title,
91154
DiagnosticId);
92-
#pragma warning restore INTL0001
155+
#pragma warning restore INTL0001
93156

94157
internal const string Description =
95158
"Implicit conversion of `DateTime` to `DateTimeOffset` determines timezone offset based on the `DateTime.Kind` value, and for `DateTimeKind.Unspecified` it assumes `DateTimeKind.Local`, which may lead to differing behavior between running locally and on a server.";
96159
}
97160
}
98-
}
161+
}

0 commit comments

Comments
 (0)