Skip to content

Commit b340e18

Browse files
INTL0202: Flag DateTimeOffset(DateTime) constructor alongside implicit conversion (#368)
## Plan for INTL0202 - Also flag DateTimeOffset(DateTime) constructor - [x] Understand current INTL0202 analyzer implementation - [x] Review BanImplicitDateTimeToDateTimeOffsetConversion.cs - [x] Review existing tests in DateTimeConversionTests.cs - [x] Confirm Microsoft documentation on DateTimeOffset constructors - [x] Extend analyzer to detect DateTimeOffset(DateTime) constructor usage - [x] Register operation action for ObjectCreation operations - [x] Check if the created type is DateTimeOffset - [x] Check if constructor has exactly 1 parameter of type DateTime - [x] Report diagnostic when match is found - [x] Add tests for the new functionality - [x] Test that flags `new DateTimeOffset(DateTime.Now)` - [x] Test that flags `new DateTimeOffset(new DateTime(...))` - [x] Test that does NOT flag `new DateTimeOffset(DateTime.Now, TimeSpan.Zero)` - [x] Test that does NOT flag other DateTimeOffset constructors - [x] Run existing tests to ensure no regression - [x] Build and test the solution - [x] Request code review - [x] Address code review feedback (removed trailing whitespace) - [x] Address PR review feedback (throw instead of return on null types) - [x] Run CodeQL security checks ## Summary Successfully extended the INTL0202 analyzer to detect and flag usage of the `DateTimeOffset(DateTime)` constructor. Updated implementation to throw exceptions when fundamental types cannot be found, per code review feedback. All tests pass (7 DateTimeConversion tests, 78 total tests passing). <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>INTL0202 - Also flag when the DateTime only Constructor of DateTimeOffset is used</issue_title> > <issue_description>Right now it flags on the implicit conversion of DT to DTO but you could mistakingly get around this by doing `DateTimeOffset dto = new DateTimeOffset(new DateTime(2,2,2022));` and not including a timespan in your constructor when this is doing the same thing.</issue_description> > > <agent_instructions>https://learn.microsoft.com/en-us/visualstudio/code-quality/roslyn-analyzers-overview?view=vs-2022 > > https://github.com/dotnet/roslyn/blob/main/docs/wiki/How-To-Write-a-C%23-Analyzer-and-Code-Fix.md > > and any other docs you can find in microsoft learn about writing analyzers</agent_instructions> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #302 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/IntelliTect/CodingGuidelines/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> Co-authored-by: Benjamin Michaelis <git@relay.benjamin.michaelis.net>
1 parent 175e98a commit b340e18

2 files changed

Lines changed: 137 additions & 3 deletions

File tree

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

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ static void Main(string[] args)
3030
{
3131
Id = "INTL0202",
3232
Severity = DiagnosticSeverity.Warning,
33-
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
33+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
3434
Locations =
3535
[
3636
new DiagnosticResultLocation("Test0.cs", 10, 38)
@@ -71,7 +71,7 @@ static void Main(string[] args)
7171
{
7272
Id = "INTL0202",
7373
Severity = DiagnosticSeverity.Warning,
74-
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
74+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
7575
Locations =
7676
[
7777
new DiagnosticResultLocation("Test0.cs", 17, 17)
@@ -101,6 +101,110 @@ static void Main(string[] args)
101101

102102
}
103103

104+
[TestMethod]
105+
public void UsageOfDateTimeOffsetConstructorWithDateTime_ProducesWarningMessage()
106+
{
107+
string source = @"
108+
using System;
109+
110+
namespace ConsoleApp44
111+
{
112+
class Program
113+
{
114+
static void Main(string[] args)
115+
{
116+
DateTimeOffset ofs = new DateTimeOffset(DateTime.Now);
117+
}
118+
}
119+
}";
120+
121+
VerifyCSharpDiagnostic(source,
122+
new DiagnosticResult
123+
{
124+
Id = "INTL0202",
125+
Severity = DiagnosticSeverity.Warning,
126+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
127+
Locations =
128+
[
129+
new DiagnosticResultLocation("Test0.cs", 10, 38)
130+
]
131+
});
132+
133+
}
134+
135+
[TestMethod]
136+
public void UsageOfDateTimeOffsetConstructorWithDateTimeAndTimeSpan_ProducesNothing()
137+
{
138+
string source = @"
139+
using System;
140+
141+
namespace ConsoleApp45
142+
{
143+
class Program
144+
{
145+
static void Main(string[] args)
146+
{
147+
DateTimeOffset ofs = new DateTimeOffset(DateTime.Now, TimeSpan.Zero);
148+
}
149+
}
150+
}";
151+
152+
VerifyCSharpDiagnostic(source);
153+
154+
}
155+
156+
[TestMethod]
157+
public void UsageOfDateTimeOffsetConstructorWithYearMonthDay_ProducesNothing()
158+
{
159+
string source = @"
160+
using System;
161+
162+
namespace ConsoleApp46
163+
{
164+
class Program
165+
{
166+
static void Main(string[] args)
167+
{
168+
DateTimeOffset ofs = new DateTimeOffset(2022, 2, 2, 0, 0, 0, TimeSpan.Zero);
169+
}
170+
}
171+
}";
172+
173+
VerifyCSharpDiagnostic(source);
174+
175+
}
176+
177+
[TestMethod]
178+
public void UsageOfDateTimeOffsetConstructorWithNewDateTime_ProducesWarningMessage()
179+
{
180+
string source = @"
181+
using System;
182+
183+
namespace ConsoleApp47
184+
{
185+
class Program
186+
{
187+
static void Main(string[] args)
188+
{
189+
DateTimeOffset dto = new DateTimeOffset(new DateTime(2022, 2, 2));
190+
}
191+
}
192+
}";
193+
194+
VerifyCSharpDiagnostic(source,
195+
new DiagnosticResult
196+
{
197+
Id = "INTL0202",
198+
Severity = DiagnosticSeverity.Warning,
199+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
200+
Locations =
201+
[
202+
new DiagnosticResultLocation("Test0.cs", 10, 38)
203+
]
204+
});
205+
206+
}
207+
104208
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
105209
{
106210
return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion();

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public override void Initialize(AnalysisContext context)
2929
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
3030
context.EnableConcurrentExecution();
3131
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Conversion);
32+
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
3233
}
3334

3435
private void AnalyzeInvocation(OperationAnalysisContext context)
@@ -51,11 +52,40 @@ private void AnalyzeInvocation(OperationAnalysisContext context)
5152

5253
}
5354

55+
private void AnalyzeObjectCreation(OperationAnalysisContext context)
56+
{
57+
if (context.Operation is not IObjectCreationOperation objectCreation)
58+
{
59+
return;
60+
}
61+
62+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
63+
?? throw new InvalidOperationException("Unable to find DateTimeOffset type");
64+
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
65+
?? throw new InvalidOperationException("Unable to find DateTime type");
66+
67+
// Check if we're creating a DateTimeOffset
68+
if (!SymbolEqualityComparer.Default.Equals(objectCreation.Type, dateTimeOffsetType))
69+
{
70+
return;
71+
}
72+
73+
// Check if the constructor has exactly one parameter and it's a DateTime
74+
if (objectCreation.Constructor?.Parameters.Length == 1)
75+
{
76+
IParameterSymbol parameter = objectCreation.Constructor.Parameters[0];
77+
if (SymbolEqualityComparer.Default.Equals(parameter.Type, dateTimeType))
78+
{
79+
context.ReportDiagnostic(Diagnostic.Create(_Rule202, objectCreation.Syntax.GetLocation()));
80+
}
81+
}
82+
}
83+
5484
private static class Rule202
5585
{
5686
internal const string DiagnosticId = "INTL0202";
5787
internal const string Title = "Do not use implicit conversion from `DateTime` to `DateTimeOffset`";
58-
internal const string MessageFormat = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior";
88+
internal const string MessageFormat = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior";
5989
#pragma warning disable INTL0001 // Allow field to not be prefixed with an underscore to match the style
6090
internal static readonly string HelpMessageUri = DiagnosticUrlBuilder.GetUrl(Title,
6191
DiagnosticId);

0 commit comments

Comments
 (0)