22using System . Collections . Immutable ;
33using System . Linq ;
44using Microsoft . CodeAnalysis ;
5+ using Microsoft . CodeAnalysis . CSharp ;
56using Microsoft . CodeAnalysis . Diagnostics ;
67using Microsoft . CodeAnalysis . Operations ;
78
@@ -30,6 +31,7 @@ public override void Initialize(AnalysisContext context)
3031 context . ConfigureGeneratedCodeAnalysis ( GeneratedCodeAnalysisFlags . Analyze | GeneratedCodeAnalysisFlags . ReportDiagnostics ) ;
3132 context . EnableConcurrentExecution ( ) ;
3233 context . RegisterOperationAction ( AnalyzeConversion , OperationKind . Conversion ) ;
34+ context . RegisterOperationAction ( AnalyzeBinaryOperation , OperationKind . Binary ) ;
3335 }
3436
3537 private void AnalyzeConversion ( OperationAnalysisContext context )
@@ -39,30 +41,74 @@ private void AnalyzeConversion(OperationAnalysisContext context)
3941 return ;
4042 }
4143
42- if ( conversionOperation . Conversion . MethodSymbol is object && conversionOperation . Conversion . MethodSymbol . ContainingType is object )
44+ ITypeSymbol sourceType = conversionOperation . Operand . Type ;
45+ ITypeSymbol targetType = conversionOperation . Type ;
46+
47+ if ( sourceType is null || targetType is null )
48+ {
49+ return ;
50+ }
51+
52+ INamedTypeSymbol dateTimeType = context . Compilation . GetTypeByMetadataName ( "System.DateTime" ) ;
53+ INamedTypeSymbol dateTimeOffsetType = context . Compilation . GetTypeByMetadataName ( "System.DateTimeOffset" ) ;
54+
55+ if ( dateTimeType is null || dateTimeOffsetType is null )
4356 {
44- INamedTypeSymbol containingType = conversionOperation . Conversion . MethodSymbol . ContainingType ;
45- if ( IsDateTimeOffsetSymbol ( context , containingType ) )
46- {
47- context . ReportDiagnostic ( Diagnostic . Create ( _Rule202 , conversionOperation . Syntax . GetLocation ( ) ) ) ;
48- }
57+ return ;
4958 }
50- else
59+
60+ // Check if source is DateTime and target is DateTimeOffset
61+ if ( SymbolEqualityComparer . Default . Equals ( sourceType , dateTimeType ) &&
62+ SymbolEqualityComparer . Default . Equals ( targetType , dateTimeOffsetType ) )
5163 {
52- IOperation implicitDateTimeOffsetOp = conversionOperation . Operand . ChildOperations
53- . Where ( op => op . Kind == OperationKind . Argument && IsDateTimeOffsetSymbol ( context , ( ( IArgumentOperation ) op ) . Value . Type ) )
54- . FirstOrDefault ( ) ;
55- if ( implicitDateTimeOffsetOp != default )
56- {
57- context . ReportDiagnostic ( Diagnostic . Create ( _Rule202 , implicitDateTimeOffsetOp . Syntax . GetLocation ( ) ) ) ;
58- }
64+ // Report the diagnostic at the operand's location (the DateTime expression being converted)
65+ context . ReportDiagnostic ( Diagnostic . Create ( _Rule202 , conversionOperation . Operand . Syntax . GetLocation ( ) ) ) ;
5966 }
6067 }
6168
62- private static bool IsDateTimeOffsetSymbol ( OperationAnalysisContext context , ITypeSymbol symbol )
69+ private void AnalyzeBinaryOperation ( OperationAnalysisContext context )
6370 {
71+ if ( context . Operation is not IBinaryOperation binaryOperation )
72+ {
73+ return ;
74+ }
75+
76+ INamedTypeSymbol dateTimeType = context . Compilation . GetTypeByMetadataName ( "System.DateTime" ) ;
6477 INamedTypeSymbol dateTimeOffsetType = context . Compilation . GetTypeByMetadataName ( "System.DateTimeOffset" ) ;
65- return SymbolEqualityComparer . Default . Equals ( symbol , dateTimeOffsetType ) ;
78+
79+ if ( dateTimeType is null || dateTimeOffsetType is null )
80+ {
81+ return ;
82+ }
83+
84+ // For binary operations, check if either operand is DateTime and the other is DateTimeOffset
85+ // This catches cases where IConversionOperation nodes are not created (e.g., property access)
86+ CheckBinaryOperandPair ( context , binaryOperation . LeftOperand , binaryOperation . RightOperand , dateTimeType , dateTimeOffsetType ) ;
87+ CheckBinaryOperandPair ( context , binaryOperation . RightOperand , binaryOperation . LeftOperand , dateTimeType , dateTimeOffsetType ) ;
88+ }
89+
90+ private void CheckBinaryOperandPair ( OperationAnalysisContext context , IOperation operand , IOperation otherOperand , INamedTypeSymbol dateTimeType , INamedTypeSymbol dateTimeOffsetType )
91+ {
92+ if ( operand is null || operand . Type is null || otherOperand is null || otherOperand . Type is null )
93+ {
94+ return ;
95+ }
96+
97+ // Skip if we don't have a syntax location to report
98+ if ( operand . Syntax is null )
99+ {
100+ return ;
101+ }
102+
103+ // Check if operand is DateTime and other operand is DateTimeOffset
104+ bool isDateTimeOperand = SymbolEqualityComparer . Default . Equals ( operand . Type , dateTimeType ) ;
105+ bool isDateTimeOffsetOtherOperand = SymbolEqualityComparer . Default . Equals ( otherOperand . Type , dateTimeOffsetType ) ;
106+
107+ if ( isDateTimeOperand && isDateTimeOffsetOtherOperand )
108+ {
109+ // DateTime will be implicitly converted to DateTimeOffset in the comparison
110+ context . ReportDiagnostic ( Diagnostic . Create ( _Rule202 , operand . Syntax . GetLocation ( ) ) ) ;
111+ }
66112 }
67113
68114 private static class Rule202
0 commit comments