- 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
133 lines
5.1 KiB
C#
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;
|
|
}
|
|
}
|