diff --git a/.github/workflows/dotnetBuild.yml b/.github/workflows/dotnetBuild.yml index 1b793870..cd858735 100644 --- a/.github/workflows/dotnetBuild.yml +++ b/.github/workflows/dotnetBuild.yml @@ -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 \ No newline at end of file + run: dotnet test --no-build --configuration Release --verbosity normal diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs index f2fec53a..12878b32 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs @@ -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; @@ -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; @@ -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 } } }"; @@ -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 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 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() { diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/Verifiers/DiagnosticVerifier.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/Verifiers/DiagnosticVerifier.cs index 134d643f..887f1a0f 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/Verifiers/DiagnosticVerifier.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/Verifiers/DiagnosticVerifier.cs @@ -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}"); } } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs index 46940659..3a148486 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -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