Skip to content
Draft
2 changes: 1 addition & 1 deletion .github/workflows/dotnetBuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ jobs:
run: dotnet build -p:ContinuousIntegrationBuild=True --no-restore --configuration Release

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
run: dotnet test --no-build --configuration Release --verbosity normal
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.CodeAnalysis;
using System;
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TestHelper;
Expand Down Expand Up @@ -31,16 +33,13 @@ static void Main(string[] args)
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations =
[
new DiagnosticResultLocation("Test0.cs", 10, 38)
]
Locations = [new DiagnosticResultLocation("Test0.cs", 10, 38)]
});

}

[TestMethod]
public void UsageOfImplicitConversionInComparison_ProducesWarningMessage()
public void UsageOfImplicitConversionInComparison_DateTimeToDateTimeOffset_ProducesWarningMessage()
{
string source = @"
using System;
Expand All @@ -53,15 +52,39 @@ internal class Program
static void Main(string[] args)
{
DateTime first = DateTime.Now;
DateTimeOffset second = DateTimeOffset.Now;
_ = first < second
}
}
}";

Thread.Sleep(10);
VerifyCSharpDiagnostic(source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 17)]
});

DateTimeOffset second = DateTimeOffset.Now;
}

if (first < second)
{
Console.WriteLine(""Time has passed..."");
}
[TestMethod]
public void UsageOfImplicitConversionInComparison_DateTimeOffsetToDateTime_ProducesWarningMessage()
{
string source = @"
using System;
using System.Threading;

namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
DateTimeOffset first = DateTimeOffset.Now;
DateTime second = DateTime.Now;
_ = first < second
}
}
}";
Expand All @@ -72,14 +95,150 @@ static void Main(string[] args)
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations =
[
new DiagnosticResultLocation("Test0.cs", 17, 17)
]
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 25)]
});

}

[TestMethod]
public void UsageOfImplicitConversionInComparison_NullableDateTimeOffsetToDateTime_ProducesWarningMessage()
{
string source = @"
using System;
using System.Threading;

namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
DateTimeOffset? first = DateTimeOffset.Now;
DateTime second = DateTime.Now;
_ = first < second
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 25)]
}
);

}

[TestMethod]
public void UsageOfImplicitConversion_WithProperties_ProducesWarningMessage()
{
string source = @"
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);

internal class Program
{
static void Main(string[] args)
{
Pair pair = new(DateTimeOffset.Now, DateTime.Now);
retirn pair.DateTimeOffset < pair.DateTime;
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 29)]
}
);
}

[TestMethod]
public void UsageOfImplicitConversion_WithPropertiesInLinq_ProducesWarningMessage()
{
string source = @"
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);

internal class Program
{
static void Main(string[] args)
{
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) }; // <-- L14
_ = list.Where(pair => pair.DateTimeOffset < pair.DateTime);
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 14, 42)]
}
);
}

[TestMethod]
public void UsageOfImplicitConversion_InLinqWithVariables_ProducesWarningMessage()
{
string source = @"
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
internal class Pair(DateTimeOffset DateTimeOffset, DateTime DateTime);

internal class Program
{
static void Main(string[] args)
{
List<Pair> list = new(){ new(DateTimeOffset.Now, DateTime.Now) };
_ = list.Where(pair => {
DateTimeOffset first = pair.DateTimeOffset;
DateTime second = pair.DateTime;
return first < second;
});
}
}
}";

VerifyCSharpDiagnostic(
source,
new DiagnosticResult
{
Id = "INTL0202",
Severity = DiagnosticSeverity.Warning,
Message = "Using the symbol 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' can result in unpredictable behavior",
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 24)]
}
);
}

[TestMethod]
public void UsageOfExplicitConversion_ProducesNothing()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,16 +167,16 @@ private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagno
// Only check line position if there is an actual line in the real diagnostic
if (actualLinePosition.Line > 0)
{
Assert.AreEqual(actualLinePosition.Line + 1,
expected.Line,
Assert.AreEqual(expected.Line,
actualLinePosition.Line + 1,
$"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\"{Environment.NewLine}{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, diagnostic)}{Environment.NewLine}");
}

// Only check column position if there is an actual column position in the real diagnostic
if (actualLinePosition.Character > 0)
{
Assert.AreEqual(actualLinePosition.Character + 1,
expected.Column,
Assert.AreEqual(expected.Column,
actualLinePosition.Character + 1,
$"Expected diagnostic to start at column \"{expected.Column}\" was actually at column \"{actualLinePosition.Character + 1}\"{Environment.NewLine}{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, diagnostic)}{Environment.NewLine}");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
Expand Down Expand Up @@ -33,22 +34,35 @@ public override void Initialize(AnalysisContext context)

private void AnalyzeInvocation(OperationAnalysisContext context)
{
if (context.Operation is not IConversionOperation conversionOperation)
if (context.Operation is not IConversionOperation conversionOperation || !conversionOperation.Conversion.IsImplicit)
{
return;
}

if (conversionOperation.Conversion.IsImplicit && conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
if (conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
{
INamedTypeSymbol containingType = conversionOperation.Conversion.MethodSymbol.ContainingType;
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
if (SymbolEqualityComparer.Default.Equals(containingType, dateTimeOffsetType))
if (IsDateTimeOffsetSymbol(context, containingType))
{
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
}
}
else
{
IOperation implicitDateTimeOffsetOp = conversionOperation.Operand.ChildOperations
.Where(op => op.Kind == OperationKind.Argument && IsDateTimeOffsetSymbol(context, ((IArgumentOperation)op).Value.Type))
.FirstOrDefault();
if (implicitDateTimeOffsetOp != default)
{
context.ReportDiagnostic(Diagnostic.Create(_Rule202, implicitDateTimeOffsetOp.Syntax.GetLocation()));
}
}
}


private static bool IsDateTimeOffsetSymbol(OperationAnalysisContext context, ITypeSymbol symbol)
{
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
return SymbolEqualityComparer.Default.Equals(symbol, dateTimeOffsetType);
}

private static class Rule202
Expand Down