Files
MilkyShots/Lactose.Analyzers/OnlyUtcDateTimeAnalyzer.cs
T
REDCODE 9d9491253b refactor: clean up imports, simplify checks, and add SearchDropdown component
- Remove unused using directives across C# and Razor files
- Remove unused IServiceProvider from SettingsRepository
- Simplify null/empty string checks in StatsRepository
- Add null-safe navigation for Albums/Tags in stats queries
- Initialize Asset.Hash default to prevent null refs
- Deduplicate AssetIds in AssetPicker
- Add OnStartedWaiting/OnFinishedWaiting/OnProgressChanged to Job
- Add global SearchDropdown component with keyboard nav
- Fix XML doc param mismatches
2026-07-12 20:47:24 +02:00

133 lines
5.1 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
namespace Lactose.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OnlyUtcDateTimeAnalyzer : DiagnosticAnalyzer
{
const string Category = "Correctness";
static readonly DiagnosticDescriptor DateTimeNowRule = new(
id: "MS001",
title: "Use DateTime.UtcNow instead of DateTime.Now",
messageFormat: "Use DateTime.UtcNow instead of DateTime.Now to ensure UTC semantics",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
static readonly DiagnosticDescriptor DateTimeTodayRule = new(
id: "MS002",
title: "Use DateTime.UtcNow.Date instead of DateTime.Today",
messageFormat: "Use DateTime.UtcNow.Date instead of DateTime.Today to ensure UTC semantics",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
static readonly DiagnosticDescriptor DateTimeDateRule = new(
id: "MS003",
title: "DateTime.Date may return non-UTC value",
messageFormat: "DateTime.Date produces a DateTime with Kind=Local; ensure this is intentional or use DateTime.UtcNow.Date",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
static readonly DiagnosticDescriptor SpecifyKindRule = new(
id: "MS004",
title: "DateTime.SpecifyKind must use DateTimeKind.Utc",
messageFormat: "DateTime.SpecifyKind with {0} is not allowed; use DateTimeKind.Utc",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(DateTimeNowRule, DateTimeTodayRule, DateTimeDateRule, SpecifyKindRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
}
void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
if (memberAccess.Name is not IdentifierNameSyntax memberName)
return;
switch (memberName.Identifier.Text)
{
case "Now":
if (IsSystemDateTime(context, memberAccess.Expression))
context.ReportDiagnostic(Diagnostic.Create(DateTimeNowRule, memberAccess.GetLocation()));
break;
case "Today":
if (IsSystemDateTime(context, memberAccess.Expression))
context.ReportDiagnostic(Diagnostic.Create(DateTimeTodayRule, memberAccess.GetLocation()));
break;
case "Date":
{
var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression, context.CancellationToken);
if (typeInfo.Type is INamedTypeSymbol namedType &&
namedType.SpecialType == SpecialType.System_DateTime)
{
context.ReportDiagnostic(Diagnostic.Create(DateTimeDateRule, memberAccess.GetLocation()));
}
break;
}
}
}
void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
return;
if (memberAccess.Name is not IdentifierNameSyntax { Identifier.Text: "SpecifyKind" })
return;
if (!IsSystemDateTime(context, memberAccess.Expression))
return;
if (invocation.ArgumentList.Arguments.Count < 2)
return;
var kindArg = invocation.ArgumentList.Arguments[1].Expression;
var kindValue = context.SemanticModel.GetConstantValue(kindArg, context.CancellationToken);
if (!kindValue.HasValue || kindValue.Value is not int intKind)
return;
var kindName = intKind switch
{
0 => "DateTimeKind.Unspecified",
1 => "DateTimeKind.Utc",
2 => "DateTimeKind.Local",
_ => $"DateTimeKind value {intKind}"
};
if (intKind != 1)
{
context.ReportDiagnostic(Diagnostic.Create(
SpecifyKindRule,
kindArg.GetLocation(),
kindName));
}
}
static bool IsSystemDateTime(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
{
var typeInfo = context.SemanticModel.GetTypeInfo(expression, context.CancellationToken);
return typeInfo.Type is INamedTypeSymbol named &&
named.SpecialType == SpecialType.System_DateTime;
}
}