Skip to content

Commit

Permalink
New rule: MA0132 do not use implicit conversion from DateTime to Date…
Browse files Browse the repository at this point in the history
…TimeOffset (#505)
  • Loading branch information
meziantou authored Apr 25, 2023
1 parent 3a7638d commit e679d84
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|⚠️|✔️||

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
|[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|<span title='Warning'>⚠️</span>|✔️||
|[MA0130](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0130.md)|Usage|GetType() should not be used on System.Type instances|<span title='Warning'>⚠️</span>|✔️||
|[MA0131](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0131.md)|Usage|ArgumentNullException.ThrowIfNull should not be used with non-nullable types|<span title='Warning'>⚠️</span>|✔️||
|[MA0132](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0132.md)|Design|Do not convert implicitly to DateTimeOffset|<span title='Warning'>⚠️</span>|✔️||

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```
14 changes: 14 additions & 0 deletions docs/Rules/MA0132.md
Original file line number Diff line number Diff line change
@@ -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
````
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DoNotImplicitlyConvertDateTimeToDateTimeOffsetAnalyzer>();
}

[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();
}
}

0 comments on commit e679d84

Please sign in to comment.