Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,128 @@ static void Main(string[] args)

}

[TestMethod]
public void UsageOfDateTimeInWhereLambda_DoesNotTriggerWarning_KnownLimitation()
{
// NOTE: This test documents a known limitation of the Roslyn operation-based analyzer.
// INTL0202 currently does NOT trigger for implicit conversions in LINQ extension method lambdas
// (e.g., System.Linq.Enumerable.Where) due to how Roslyn handles conversion operations
// in these contexts.
//
// Related: https://github.com/dotnet/roslyn/issues/14722
//
// This limitation affects:
// - Extension methods from System.Linq.Enumerable (Where, Select, etc.)
// - Potentially other extension method lambdas
//
// The analyzer DOES work correctly for:
// - Direct comparisons (e.g., if (dateTime < dateTimeOffset))
// - Local function calls (see UsageOfDateTimeInLocalFunction_ProducesWarningMessage)
// - Instance method lambdas (e.g., List<T>.RemoveAll - see UsageOfDateTimeInRemoveAllLambda_ProducesWarningMessage)
//
// TODO: Investigate syntax-based analysis or other workarounds to detect these cases

string source = @"
using System;
using System.Linq;
using System.Collections.Generic;

namespace ConsoleApp48
{
class Program
{
static void Main(string[] args)
{
var list = new List<DateTimeOffset>();
DateTime dt = DateTime.Now;

_ = list.Where(item => item < dt);
}
}
}";

// Currently no warnings - this documents the known limitation
// Explicitly expecting zero diagnostics
VerifyCSharpDiagnostic(source);
}

[TestMethod]
public void UsageOfDateTimeInRemoveAllLambda_ProducesWarningMessage()
{
// This test shows that RemoveAll lambda DOES trigger warning
string source = @"
using System;
using System.Collections.Generic;

namespace ConsoleApp48a
{
class Program
{
static void Main(string[] args)
{
var list = new List<DateTimeOffset>();
DateTime dt = DateTime.Now;

list.RemoveAll(item => item < dt);
}
}
}";

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

}

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

namespace ConsoleApp49
{
class Program
{
static void Main(string[] args)
{
var list = new List<DateTimeOffset>();
DateTime dt = DateTime.Now;

bool Compare(DateTimeOffset item)
{
return item < dt;
}

var query = list.Where(Compare);
}
}
}";

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

}

protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,67 @@ public override void Initialize(AnalysisContext context)

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Conversion);
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);

context.RegisterCompilationStartAction(compilationContext =>
{
INamedTypeSymbol dateTimeOffsetType = compilationContext.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
INamedTypeSymbol dateTimeType = compilationContext.Compilation.GetTypeByMetadataName("System.DateTime");

if (dateTimeOffsetType is null || dateTimeType is null)
{
return;
}

compilationContext.RegisterOperationAction(
operationContext => AnalyzeConversion(operationContext, dateTimeOffsetType, dateTimeType),
OperationKind.Conversion);
compilationContext.RegisterOperationAction(
operationContext => AnalyzeObjectCreation(operationContext, dateTimeOffsetType, dateTimeType),
OperationKind.ObjectCreation);
});
}

private void AnalyzeInvocation(OperationAnalysisContext context)

private void AnalyzeConversion(OperationAnalysisContext context, INamedTypeSymbol dateTimeOffsetType, INamedTypeSymbol dateTimeType)
{
if (context.Operation is not IConversionOperation conversionOperation)
{
return;
}

if (conversionOperation.Conversion.IsImplicit && conversionOperation.Conversion.MethodSymbol is object && conversionOperation.Conversion.MethodSymbol.ContainingType is object)
if (!conversionOperation.Conversion.IsImplicit)
{
return;
}

// Check via method symbol (original logic)
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))
{
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
return;
}
}


// Fallback: Check via operand and type information
// This handles cases where MethodSymbol might not be populated (e.g., in some lambda contexts)
if (SymbolEqualityComparer.Default.Equals(conversionOperation.Operand?.Type, dateTimeType) &&
SymbolEqualityComparer.Default.Equals(conversionOperation.Type, dateTimeOffsetType))
{
context.ReportDiagnostic(Diagnostic.Create(_Rule202, conversionOperation.Syntax.GetLocation()));
}
}

private void AnalyzeObjectCreation(OperationAnalysisContext context)
private void AnalyzeObjectCreation(OperationAnalysisContext context, INamedTypeSymbol dateTimeOffsetType, INamedTypeSymbol dateTimeType)
{
if (context.Operation is not IObjectCreationOperation objectCreation)
{
return;
}

INamedTypeSymbol dateTimeOffsetType = context.Compilation.GetTypeByMetadataName("System.DateTimeOffset")
?? throw new InvalidOperationException("Unable to find DateTimeOffset type");
INamedTypeSymbol dateTimeType = context.Compilation.GetTypeByMetadataName("System.DateTime")
?? throw new InvalidOperationException("Unable to find DateTime type");

// Check if we're creating a DateTimeOffset
if (!SymbolEqualityComparer.Default.Equals(objectCreation.Type, dateTimeOffsetType))
{
Expand Down