Skip to content

Commit e7bd995

Browse files
Add INTL0202 detection for DateTime to DateTimeOffset conversions in all contexts
Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com>
1 parent 4a8b1bb commit e7bd995

File tree

2 files changed

+192
-17
lines changed

2 files changed

+192
-17
lines changed

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

Lines changed: 171 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 the symbol 'DateTimeOffset.implicit operator 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,149 @@ static void Main(string[] args)
7398
Severity = DiagnosticSeverity.Warning,
7499
Message = "Using the symbol 'DateTimeOffset.implicit operator 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 the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
133+
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 25)]
134+
}
135+
);
136+
}
137+
138+
[TestMethod]
139+
public void UsageOfImplicitConversion_WithProperties_ProducesWarningMessage()
140+
{
141+
string source = @"
142+
using System;
143+
using System.Collections.Generic;
144+
using System.Linq;
145+
146+
namespace ConsoleApp1
147+
{
148+
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);
149+
150+
internal class Program
151+
{
152+
static void Main(string[] args)
153+
{
154+
Pair pair = new(DateTimeOffset.Now, DateTime.Now);
155+
_ = pair.DateTimeOffset < pair.DateTime;
156+
}
157+
}
158+
}";
159+
160+
VerifyCSharpDiagnostic(
161+
source,
162+
new DiagnosticResult
163+
{
164+
Id = "INTL0202",
165+
Severity = DiagnosticSeverity.Warning,
166+
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
167+
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 29)]
168+
}
169+
);
170+
}
171+
172+
[TestMethod]
173+
public void UsageOfImplicitConversion_WithPropertiesInLinq_ProducesWarningMessage()
174+
{
175+
string source = @"
176+
using System;
177+
using System.Collections.Generic;
178+
using System.Linq;
179+
180+
namespace ConsoleApp1
181+
{
182+
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);
183+
184+
internal class Program
185+
{
186+
static void Main(string[] args)
187+
{
188+
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) };
189+
_ = list.Where(pair => pair.DateTimeOffset < pair.DateTime);
190+
}
191+
}
192+
}";
193+
194+
VerifyCSharpDiagnostic(
195+
source,
196+
new DiagnosticResult
197+
{
198+
Id = "INTL0202",
199+
Severity = DiagnosticSeverity.Warning,
200+
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
201+
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 42)]
202+
}
203+
);
204+
}
205+
206+
[TestMethod]
207+
public void UsageOfImplicitConversion_InLinqWithVariables_ProducesWarningMessage()
208+
{
209+
string source = @"
210+
using System;
211+
using System.Collections.Generic;
212+
using System.Linq;
213+
214+
namespace ConsoleApp1
215+
{
216+
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);
217+
218+
internal class Program
219+
{
220+
static void Main(string[] args)
221+
{
222+
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) };
223+
_ = list.Where(pair => {
224+
DateTimeOffset first = pair.DateTimeOffset;
225+
DateTime second = pair.DateTime;
226+
return first < second;
227+
});
228+
}
229+
}
230+
}";
231+
232+
VerifyCSharpDiagnostic(
233+
source,
234+
new DiagnosticResult
235+
{
236+
Id = "INTL0202",
237+
Severity = DiagnosticSeverity.Warning,
238+
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
239+
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 24)]
240+
}
241+
);
242+
}
243+
83244
[TestMethod]
84245
public void UsageOfExplicitConversion_ProducesNothing()
85246
{

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Immutable;
3+
using System.Linq;
34
using Microsoft.CodeAnalysis;
45
using Microsoft.CodeAnalysis.Diagnostics;
56
using Microsoft.CodeAnalysis.Operations;
@@ -28,27 +29,40 @@ public override void Initialize(AnalysisContext context)
2829

2930
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
3031
context.EnableConcurrentExecution();
31-
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Conversion);
32+
context.RegisterOperationAction(AnalyzeConversion, OperationKind.Conversion);
3233
}
3334

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

41-
if (conversionOperation.Conversion.IsImplicit && conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
42+
if (conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
4243
{
4344
INamedTypeSymbol containingType = conversionOperation.Conversion.MethodSymbol.ContainingType;
44-
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
45-
if (SymbolEqualityComparer.Default.Equals(containingType, dateTimeOffsetType))
45+
if (IsDateTimeOffsetSymbol(context, containingType))
4646
{
4747
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
4848
}
4949
}
50+
else
51+
{
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+
}
59+
}
60+
}
5061

51-
62+
private static bool IsDateTimeOffsetSymbol(OperationAnalysisContext context, ITypeSymbol symbol)
63+
{
64+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
65+
return SymbolEqualityComparer.Default.Equals(symbol, dateTimeOffsetType);
5266
}
5367

5468
private static class Rule202

0 commit comments

Comments
 (0)