diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs index f2fec53a..5b1973b4 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs @@ -30,7 +30,7 @@ static void Main(string[] args) { Id = "INTL0202", Severity = DiagnosticSeverity.Warning, - Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior", + Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior", Locations = [ new DiagnosticResultLocation("Test0.cs", 10, 38) @@ -71,7 +71,7 @@ static void Main(string[] args) { Id = "INTL0202", Severity = DiagnosticSeverity.Warning, - Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior", + Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior", Locations = [ new DiagnosticResultLocation("Test0.cs", 17, 17) @@ -101,6 +101,110 @@ static void Main(string[] args) } + [TestMethod] + public void UsageOfDateTimeOffsetConstructorWithDateTime_ProducesWarningMessage() + { + string source = @" +using System; + +namespace ConsoleApp44 + { + class Program + { + static void Main(string[] args) + { + DateTimeOffset ofs = new DateTimeOffset(DateTime.Now); + } + } + }"; + + VerifyCSharpDiagnostic(source, + new DiagnosticResult + { + Id = "INTL0202", + Severity = DiagnosticSeverity.Warning, + Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior", + Locations = + [ + new DiagnosticResultLocation("Test0.cs", 10, 38) + ] + }); + + } + + [TestMethod] + public void UsageOfDateTimeOffsetConstructorWithDateTimeAndTimeSpan_ProducesNothing() + { + string source = @" +using System; + +namespace ConsoleApp45 + { + class Program + { + static void Main(string[] args) + { + DateTimeOffset ofs = new DateTimeOffset(DateTime.Now, TimeSpan.Zero); + } + } + }"; + + VerifyCSharpDiagnostic(source); + + } + + [TestMethod] + public void UsageOfDateTimeOffsetConstructorWithYearMonthDay_ProducesNothing() + { + string source = @" +using System; + +namespace ConsoleApp46 + { + class Program + { + static void Main(string[] args) + { + DateTimeOffset ofs = new DateTimeOffset(2022, 2, 2, 0, 0, 0, TimeSpan.Zero); + } + } + }"; + + VerifyCSharpDiagnostic(source); + + } + + [TestMethod] + public void UsageOfDateTimeOffsetConstructorWithNewDateTime_ProducesWarningMessage() + { + string source = @" +using System; + +namespace ConsoleApp47 + { + class Program + { + static void Main(string[] args) + { + DateTimeOffset dto = new DateTimeOffset(new DateTime(2022, 2, 2)); + } + } + }"; + + VerifyCSharpDiagnostic(source, + new DiagnosticResult + { + Id = "INTL0202", + Severity = DiagnosticSeverity.Warning, + Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior", + Locations = + [ + new DiagnosticResultLocation("Test0.cs", 10, 38) + ] + }); + + } + protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() { return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion(); diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs index 46940659..303e11f1 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs @@ -29,6 +29,7 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Conversion); + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); } private void AnalyzeInvocation(OperationAnalysisContext context) @@ -51,11 +52,40 @@ private void AnalyzeInvocation(OperationAnalysisContext context) } + private void AnalyzeObjectCreation(OperationAnalysisContext context) + { + if (context.Operation is not IObjectCreationOperation objectCreation) + { + return; + } + + INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset") + ?? throw new InvalidOperationException("Unable to find DateTimeOffset type"); + INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime") + ?? throw new InvalidOperationException("Unable to find DateTime type"); + + // Check if we're creating a DateTimeOffset + if (!SymbolEqualityComparer.Default.Equals(objectCreation.Type, dateTimeOffsetType)) + { + return; + } + + // Check if the constructor has exactly one parameter and it's a DateTime + if (objectCreation.Constructor?.Parameters.Length == 1) + { + IParameterSymbol parameter = objectCreation.Constructor.Parameters[0]; + if (SymbolEqualityComparer.Default.Equals(parameter.Type, dateTimeType)) + { + context.ReportDiagnostic(Diagnostic.Create(_Rule202, objectCreation.Syntax.GetLocation())); + } + } + } + private static class Rule202 { internal const string DiagnosticId = "INTL0202"; internal const string Title = "Do not use implicit conversion from `DateTime` to `DateTimeOffset`"; - internal const string MessageFormat = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior"; + internal const string MessageFormat = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior"; #pragma warning disable INTL0001 // Allow field to not be prefixed with an underscore to match the style internal static readonly string HelpMessageUri = DiagnosticUrlBuilder.GetUrl(Title, DiagnosticId);