From e679d8471f5b9da9b7e5671519a66767ed16bd4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 24 Apr 2023 21:49:44 -0400 Subject: [PATCH] New rule: MA0132 do not use implicit conversion from DateTime to DateTimeOffset (#505) --- README.md | 1 + docs/README.md | 7 +++ docs/Rules/MA0132.md | 14 +++++ src/Meziantou.Analyzer/RuleIdentifiers.cs | 1 + ...ConvertDateTimeToDateTimeOffsetAnalyzer.cs | 54 +++++++++++++++++++ ...DateTimeWithDateTimeOffsetAnalyzerTests.cs | 52 ++++++++++++++++++ 6 files changed, 129 insertions(+) create mode 100644 docs/Rules/MA0132.md create mode 100644 src/Meziantou.Analyzer/Rules/DoNotImplicitlyConvertDateTimeToDateTimeOffsetAnalyzer.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/DoNotCompareDateTimeWithDateTimeOffsetAnalyzerTests.cs diff --git a/README.md b/README.md index d52dce74b..046c14a44 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|❌| |[MA0130](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0130.md)|Usage|GetType() should not be used on System.Type instances|⚠️|✔️|❌| |[MA0131](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0131.md)|Usage|ArgumentNullException.ThrowIfNull should not be used with non-nullable types|⚠️|✔️|❌| +|[MA0132](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0132.md)|Design|Do not convert implicitly to DateTimeOffset|⚠️|✔️|❌| diff --git a/docs/README.md b/docs/README.md index 7b9a0de46..bc881e162 100644 --- a/docs/README.md +++ b/docs/README.md @@ -131,6 +131,7 @@ |[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|❌| |[MA0130](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0130.md)|Usage|GetType() should not be used on System.Type instances|⚠️|✔️|❌| |[MA0131](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0131.md)|Usage|ArgumentNullException.ThrowIfNull should not be used with non-nullable types|⚠️|✔️|❌| +|[MA0132](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0132.md)|Design|Do not convert implicitly to DateTimeOffset|⚠️|✔️|❌| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -530,6 +531,9 @@ dotnet_diagnostic.MA0130.severity = warning # MA0131: ArgumentNullException.ThrowIfNull should not be used with non-nullable types dotnet_diagnostic.MA0131.severity = warning + +# MA0132: Do not convert implicitly to DateTimeOffset +dotnet_diagnostic.MA0132.severity = warning ``` # .editorconfig - all rules disabled @@ -924,4 +928,7 @@ dotnet_diagnostic.MA0130.severity = none # MA0131: ArgumentNullException.ThrowIfNull should not be used with non-nullable types dotnet_diagnostic.MA0131.severity = none + +# MA0132: Do not convert implicitly to DateTimeOffset +dotnet_diagnostic.MA0132.severity = none ``` diff --git a/docs/Rules/MA0132.md b/docs/Rules/MA0132.md new file mode 100644 index 000000000..06da8f7ca --- /dev/null +++ b/docs/Rules/MA0132.md @@ -0,0 +1,14 @@ +# MA0132 - Do not convert implicitly to DateTimeOffset + +Implicit conversions from `DateTime` to `DateTimeOffset` are dangerous. The result depends on `DateTime.Kind` which is often `Unspecified`, and so, fallback to a local time. +This may not be desired. Also, this may indicate that you are mixing `DateTime` and `DateTimeOffset` in your application, which may be unintentional. + +````c# +DateTime dt = ...; + +dt - DateTimeOffset.UtcNow; // non-compliant as there is an implicit conversion from DateTime to DateTimeOffset + +DateTimeOffset.UtcNow - DateTimeOffset.UtcNow; // ok +new DateTimeOffset(dt) - DateTimeOffset.UtcNow; // ok +(DateTimeOffset)dt - DateTimeOffset.UtcNow; // ok +```` diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 79fa6ffa3..5ace6dc40 100644 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -134,6 +134,7 @@ internal static class RuleIdentifiers public const string TaskInUsing = "MA0129"; public const string ObjectGetTypeOnTypeInstance = "MA0130"; public const string ThrowIfNullWithNonNullableInstance = "MA0131"; + public const string DoNotImplicitlyConvertDateTimeToDateTimeOffset = "MA0132"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/DoNotImplicitlyConvertDateTimeToDateTimeOffsetAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotImplicitlyConvertDateTimeToDateTimeOffsetAnalyzer.cs new file mode 100644 index 000000000..4646ff145 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/DoNotImplicitlyConvertDateTimeToDateTimeOffsetAnalyzer.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class DoNotImplicitlyConvertDateTimeToDateTimeOffsetAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor s_rule = new( + RuleIdentifiers.DoNotImplicitlyConvertDateTimeToDateTimeOffset, + title: "Do not convert implicitly to DateTimeOffset", + messageFormat: "Do not convert implicitly to DateTimeOffset", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotImplicitlyConvertDateTimeToDateTimeOffset)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(s_rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(context => + { + var datetimeOffsetSymbol = context.Compilation.GetBestTypeByMetadataName("System.DateTimeOffset"); + if (datetimeOffsetSymbol == null) + return; + + context.RegisterOperationAction(context => AnalyzeConversion(context, datetimeOffsetSymbol), OperationKind.Conversion); + }); + } + + private static void AnalyzeConversion(OperationAnalysisContext context, INamedTypeSymbol dateTimeOffsetSymbol) + { + var operation = (IConversionOperation)context.Operation; + if (!operation.Conversion.IsImplicit) + return; + + if (operation.Type.IsEqualTo(dateTimeOffsetSymbol) && operation.Operand.Type.IsDateTime()) + { + // DateTime.Now and DateTime.UtcNow set the DateTime.Kind, so the conversion result is well-known + if (operation.Operand is IMemberReferenceOperation { Member.Name: "UtcNow" or "Now", Member.ContainingType.SpecialType: SpecialType.System_DateTime }) + return; + + context.ReportDiagnostic(s_rule, operation); + } + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotCompareDateTimeWithDateTimeOffsetAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotCompareDateTimeWithDateTimeOffsetAnalyzerTests.cs new file mode 100644 index 000000000..eba53d121 --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotCompareDateTimeWithDateTimeOffsetAnalyzerTests.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Meziantou.Analyzer.Rules; +using Microsoft.CodeAnalysis; +using TestHelper; +using Xunit; + +namespace Meziantou.Analyzer.Test.Rules; +public class DoNotCompareDateTimeWithDateTimeOffsetAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithOutputKind(OutputKind.ConsoleApplication) + .WithAnalyzer(); + } + + [Fact] + public async Task ImplicitConversion_BinaryOperation_Subtract_UtcNow() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System; + + _ = DateTime.UtcNow - DateTimeOffset.UtcNow; + """) + .ValidateAsync(); + } + + [Fact] + public async Task ImplicitConversion_BinaryOperation_Subtract() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System; + + _ = [|default(DateTime)|] - DateTimeOffset.UtcNow; + """) + .ValidateAsync(); + } + + [Fact] + public async Task ExplicitConversion_BinaryOperation_Subtract() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System; + + _ = (DateTimeOffset)default(DateTime) - DateTimeOffset.UtcNow; + """) + .ValidateAsync(); + } +}