Skip to content

Commit 838e0a0

Browse files
work
1 parent d4e7320 commit 838e0a0

2 files changed

Lines changed: 302 additions & 11 deletions

File tree

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,249 @@ static void Main(string[] args)
419419
});
420420
}
421421

422+
[TestMethod]
423+
public void DateTimeOffsetLessThanDateTime_ProducesDiagnostic()
424+
{
425+
string source = @"
426+
using System;
427+
using System.Threading;
428+
429+
namespace ConsoleApp1
430+
{
431+
internal class Program
432+
{
433+
static void Main(string[] args)
434+
{
435+
DateTimeOffset first = DateTimeOffset.Now;
436+
437+
Thread.Sleep(10);
438+
439+
DateTime second = DateTime.Now;
440+
441+
if (first < second)
442+
{
443+
Console.WriteLine(""Time has passed..."");
444+
}
445+
}
446+
}
447+
}";
448+
449+
VerifyCSharpDiagnostic(source,
450+
new DiagnosticResult
451+
{
452+
Id = "INTL0202",
453+
Severity = DiagnosticSeverity.Warning,
454+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
455+
Locations =
456+
[
457+
new DiagnosticResultLocation("Test0.cs", 17, 25)
458+
]
459+
});
460+
}
461+
462+
[TestMethod]
463+
public void NullableDateTimeOffsetLessThanDateTime_ProducesDiagnostic()
464+
{
465+
string source = @"
466+
using System;
467+
using System.Threading;
468+
469+
namespace ConsoleApp1
470+
{
471+
internal class Program
472+
{
473+
static void Main(string[] args)
474+
{
475+
DateTimeOffset? first = DateTimeOffset.Now;
476+
477+
Thread.Sleep(10);
478+
479+
DateTime second = DateTime.Now;
480+
481+
if (first < second)
482+
{
483+
Console.WriteLine(""Time has passed..."");
484+
}
485+
}
486+
}
487+
}";
488+
489+
VerifyCSharpDiagnostic(source,
490+
new DiagnosticResult
491+
{
492+
Id = "INTL0202",
493+
Severity = DiagnosticSeverity.Warning,
494+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
495+
Locations =
496+
[
497+
new DiagnosticResultLocation("Test0.cs", 17, 25)
498+
]
499+
});
500+
}
501+
502+
[Ignore]
503+
[TestMethod]
504+
public void PropertyBasedDateTimeLessThanDateTimeOffset_ProducesDiagnostic()
505+
{
506+
string source = @"
507+
using System;
508+
509+
namespace ConsoleApp1
510+
{
511+
class Pair
512+
{
513+
public DateTime DateTime { get; set; }
514+
public DateTimeOffset DateTimeOffset { get; set; }
515+
}
516+
517+
internal class Program
518+
{
519+
static void Main(string[] args)
520+
{
521+
Pair pair = new Pair();
522+
_ = pair.DateTime < pair.DateTimeOffset;
523+
}
524+
}
525+
}";
526+
527+
VerifyCSharpDiagnostic(source,
528+
new DiagnosticResult
529+
{
530+
Id = "INTL0202",
531+
Severity = DiagnosticSeverity.Warning,
532+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
533+
Locations =
534+
[
535+
new DiagnosticResultLocation("Test0.cs", 18, 17)
536+
]
537+
});
538+
}
539+
540+
[Ignore]
541+
[TestMethod]
542+
public void PropertyComparisonInsideLinqLambda_ProducesDiagnostic()
543+
{
544+
string source = @"
545+
using System;
546+
using System.Collections.Generic;
547+
using System.Linq;
548+
549+
namespace ConsoleApp1
550+
{
551+
class Pair
552+
{
553+
public DateTime DateTime { get; set; }
554+
public DateTimeOffset DateTimeOffset { get; set; }
555+
}
556+
557+
internal class Program
558+
{
559+
static void Main(string[] args)
560+
{
561+
List<Pair> list = new List<Pair>();
562+
_ = list.Where(pair => pair.DateTime < pair.DateTimeOffset);
563+
}
564+
}
565+
}";
566+
567+
VerifyCSharpDiagnostic(source,
568+
new DiagnosticResult
569+
{
570+
Id = "INTL0202",
571+
Severity = DiagnosticSeverity.Warning,
572+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
573+
Locations =
574+
[
575+
new DiagnosticResultLocation("Test0.cs", 20, 36)
576+
]
577+
});
578+
}
579+
580+
[Ignore]
581+
[TestMethod]
582+
public void ExtractedPropertyVariablesInLinqLambda_ProducesDiagnostic()
583+
{
584+
string source = @"
585+
using System;
586+
using System.Collections.Generic;
587+
using System.Linq;
588+
589+
namespace ConsoleApp1
590+
{
591+
class Pair
592+
{
593+
public DateTime DateTime { get; set; }
594+
public DateTimeOffset DateTimeOffset { get; set; }
595+
}
596+
597+
internal class Program
598+
{
599+
static void Main(string[] args)
600+
{
601+
List<Pair> list = new List<Pair>();
602+
_ = list.Where(pair =>
603+
{
604+
DateTime dt = pair.DateTime;
605+
DateTimeOffset dto = pair.DateTimeOffset;
606+
return dt < dto;
607+
});
608+
}
609+
}
610+
}";
611+
612+
VerifyCSharpDiagnostic(source,
613+
new DiagnosticResult
614+
{
615+
Id = "INTL0202",
616+
Severity = DiagnosticSeverity.Warning,
617+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
618+
Locations =
619+
[
620+
new DiagnosticResultLocation("Test0.cs", 24, 24)
621+
]
622+
});
623+
}
624+
625+
[Ignore]
626+
[TestMethod]
627+
public void IQueryableWhereWithDateTimePropertyComparison_ProducesDiagnostic()
628+
{
629+
string source = @"
630+
using System;
631+
using System.Linq;
632+
633+
namespace ConsoleApp1
634+
{
635+
class TimeEntry
636+
{
637+
public DateTimeOffset EndDate { get; set; }
638+
}
639+
640+
internal class Program
641+
{
642+
static DateTimeOffset EndDate { get; set; }
643+
644+
static void Main(string[] args)
645+
{
646+
IQueryable<TimeEntry> entries = null;
647+
_ = entries.Where(te => te.EndDate <= EndDate.Date.AddDays(1).AddTicks(-1));
648+
}
649+
}
650+
}";
651+
652+
VerifyCSharpDiagnostic(source,
653+
new DiagnosticResult
654+
{
655+
Id = "INTL0202",
656+
Severity = DiagnosticSeverity.Warning,
657+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
658+
Locations =
659+
[
660+
new DiagnosticResultLocation("Test0.cs", 20, 44)
661+
]
662+
});
663+
}
664+
422665
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
423666
{
424667
return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion();

IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/BanImplicitDateTimeToDateTimeOffsetConversion.cs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public override void Initialize(AnalysisContext context)
3030
context.EnableConcurrentExecution();
3131
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Conversion);
3232
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
33+
context.RegisterOperationAction(AnalyzeBinaryOperation, OperationKind.Binary);
3334
}
3435

3536
private void AnalyzeInvocation(OperationAnalysisContext context)
@@ -42,11 +43,8 @@ private void AnalyzeInvocation(OperationAnalysisContext context)
4243
if (conversionOperation.Conversion.IsImplicit && conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
4344
{
4445
INamedTypeSymbol containingType = conversionOperation.Conversion.MethodSymbol.ContainingType;
45-
INamedTypeSymbol? dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
46-
if (dateTimeOffsetType is null)
47-
{
48-
return;
49-
}
46+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
47+
?? throw new InvalidOperationException("System.DateTimeOffset type not found in compilation");
5048
if (SymbolEqualityComparer.Default.Equals(containingType, dateTimeOffsetType))
5149
{
5250
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
@@ -63,12 +61,10 @@ private void AnalyzeObjectCreation(OperationAnalysisContext context)
6361
return;
6462
}
6563

66-
INamedTypeSymbol? dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
67-
INamedTypeSymbol? dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime");
68-
if (dateTimeOffsetType is null || dateTimeType is null)
69-
{
70-
return;
71-
}
64+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
65+
?? throw new InvalidOperationException("System.DateTimeOffset type not found in compilation");
66+
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
67+
?? throw new InvalidOperationException("System.DateTime type not found in compilation");
7268

7369
// Check if we're creating a DateTimeOffset
7470
if (!SymbolEqualityComparer.Default.Equals(objectCreation.Type, dateTimeOffsetType))
@@ -87,6 +83,58 @@ private void AnalyzeObjectCreation(OperationAnalysisContext context)
8783
}
8884
}
8985

86+
private void AnalyzeBinaryOperation(OperationAnalysisContext context)
87+
{
88+
if (context.Operation is not IBinaryOperation binaryOperation)
89+
{
90+
return;
91+
}
92+
93+
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
94+
?? throw new InvalidOperationException("System.DateTime type not found in compilation");
95+
INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
96+
?? throw new InvalidOperationException("System.DateTimeOffset type not found in compilation");
97+
98+
CheckBinaryOperandPair(context, binaryOperation.LeftOperand, binaryOperation.RightOperand, dateTimeType, dateTimeOffsetType);
99+
CheckBinaryOperandPair(context, binaryOperation.RightOperand, binaryOperation.LeftOperand, dateTimeType, dateTimeOffsetType);
100+
}
101+
102+
private static void CheckBinaryOperandPair(OperationAnalysisContext context, IOperation operand, IOperation otherOperand, INamedTypeSymbol dateTimeType, INamedTypeSymbol dateTimeOffsetType)
103+
{
104+
if (operand?.Type is null || otherOperand?.Type is null)
105+
{
106+
return;
107+
}
108+
109+
if (operand.Syntax is null)
110+
{
111+
return;
112+
}
113+
114+
// Skip if the operand is already a conversion — the conversion handler catches those
115+
if (operand is IConversionOperation)
116+
{
117+
return;
118+
}
119+
120+
// Unwrap nullable types if present
121+
ITypeSymbol operandType = operand.Type is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable
122+
? nullable.TypeArguments[0]
123+
: operand.Type;
124+
125+
ITypeSymbol otherType = otherOperand.Type is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } otherNullable
126+
? otherNullable.TypeArguments[0]
127+
: otherOperand.Type;
128+
129+
bool isDateTimeOperand = SymbolEqualityComparer.Default.Equals(operandType, dateTimeType);
130+
bool isDateTimeOffsetOtherOperand = SymbolEqualityComparer.Default.Equals(otherType, dateTimeOffsetType);
131+
132+
if (isDateTimeOperand && isDateTimeOffsetOtherOperand)
133+
{
134+
context.ReportDiagnostic(Diagnostic.Create(_Rule202, operand.Syntax.GetLocation()));
135+
}
136+
}
137+
90138
private static class Rule202
91139
{
92140
internal const string DiagnosticId = "INTL0202";

0 commit comments

Comments
 (0)