Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0185](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0185.md)|Performance|Simplify string.Create when all parameters are culture invariant|ℹ️|✔️|✔️|
|[MA0186](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0186.md)|Design|Equals method should use \[NotNullWhen(true)\] on the parameter|ℹ️|❌|❌|
|[MA0187](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0187.md)|Design|Use constructor injection instead of \[Inject\] attribute|ℹ️|❌|✔️|
|[MA0188](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0188.md)|Design|Use System.TimeProvider instead of a custom time abstraction|ℹ️|✔️|❌|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
|[MA0185](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0185.md)|Performance|Simplify string.Create when all parameters are culture invariant|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0186](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0186.md)|Design|Equals method should use \[NotNullWhen(true)\] on the parameter|<span title='Info'>ℹ️</span>|❌|❌|
|[MA0187](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0187.md)|Design|Use constructor injection instead of \[Inject\] attribute|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0188](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0188.md)|Design|Use System.TimeProvider instead of a custom time abstraction|<span title='Info'>ℹ️</span>|✔️|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -759,6 +760,9 @@ dotnet_diagnostic.MA0186.severity = none

# MA0187: Use constructor injection instead of [Inject] attribute
dotnet_diagnostic.MA0187.severity = none

# MA0188: Use System.TimeProvider instead of a custom time abstraction
dotnet_diagnostic.MA0188.severity = suggestion
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1318,4 +1322,7 @@ dotnet_diagnostic.MA0186.severity = none

# MA0187: Use constructor injection instead of [Inject] attribute
dotnet_diagnostic.MA0187.severity = none

# MA0188: Use System.TimeProvider instead of a custom time abstraction
dotnet_diagnostic.MA0188.severity = none
```
39 changes: 39 additions & 0 deletions docs/Rules/MA0188.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# MA0188 - Use System.TimeProvider instead of a custom time abstraction
Comment thread
meziantou marked this conversation as resolved.
<!-- sources -->
Source: [UseTimeProviderInsteadOfInterfaceAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseTimeProviderInsteadOfInterfaceAnalyzer.cs)
<!-- sources -->

`System.TimeProvider` (available since .NET 8) provides a standard abstraction for time-related operations. Defining a custom interface for this purpose is unnecessary when `System.TimeProvider` covers the same use case. For older target frameworks, the [`Microsoft.Bcl.TimeProvider`](https://www.nuget.org/packages/Microsoft.Bcl.TimeProvider) NuGet package provides a backport.

This rule reports interfaces whose members consist entirely of time-related properties or methods (`Now`, `UtcNow`, `GetNow()`, `GetUtcNow()`) returning `DateTime` or `DateTimeOffset`.

## Non-compliant code

```csharp
interface ITimeProvider
{
System.DateTime UtcNow { get; }
}
```

```csharp
interface ITimeService
{
System.DateTimeOffset GetUtcNow();
}
```

## Compliant code

Use `System.TimeProvider` directly instead of defining a custom interface:

```csharp
class Sample
{
void Usage(System.TimeProvider timeProvider)
{
var now = timeProvider.GetUtcNow();
}
}
```

Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,6 @@ dotnet_diagnostic.MA0186.severity = none

# MA0187: Use constructor injection instead of [Inject] attribute
dotnet_diagnostic.MA0187.severity = none

# MA0188: Use System.TimeProvider instead of a custom time abstraction
dotnet_diagnostic.MA0188.severity = suggestion
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,6 @@ dotnet_diagnostic.MA0186.severity = none

# MA0187: Use constructor injection instead of [Inject] attribute
dotnet_diagnostic.MA0187.severity = none

# MA0188: Use System.TimeProvider instead of a custom time abstraction
dotnet_diagnostic.MA0188.severity = none
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ internal static class RuleIdentifiers
public const string SimplifyStringCreateWhenAllParametersAreCultureInvariant = "MA0185";
public const string MissingNotNullWhenAttributeOnEquals = "MA0186";
public const string BlazorPropertyInjectionShouldUseConstructorInjection = "MA0187";
public const string UseTimeProviderInsteadOfInterface = "MA0188";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseTimeProviderInsteadOfInterfaceAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.UseTimeProviderInsteadOfInterface,
title: "Use System.TimeProvider instead of a custom time abstraction",
messageFormat: "Use System.TimeProvider instead of defining a custom time abstraction",
RuleCategories.Design,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseTimeProviderInsteadOfInterface));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(ctx =>
{
var dateTimeSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.DateTime");
var dateTimeOffsetSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.DateTimeOffset");

if (dateTimeSymbol is null && dateTimeOffsetSymbol is null)
return;

ctx.RegisterSymbolAction(ctx => AnalyzeNamedType(ctx, dateTimeSymbol, dateTimeOffsetSymbol), SymbolKind.NamedType);
});
}

private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol? dateTimeSymbol, INamedTypeSymbol? dateTimeOffsetSymbol)
{
var type = (INamedTypeSymbol)context.Symbol;
if (type.TypeKind is not TypeKind.Interface)
return;

// Must have at least one member
var members = type.GetMembers();
if (members.IsEmpty)
return;

// All members must be time-provider-like (skip property accessor methods as they're covered by property symbols)
foreach (var member in members)
{
// Skip property accessor methods - they are represented by the IPropertySymbol
if (member is IMethodSymbol { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet })
continue;

if (!IsTimeProviderLikeMember(member, dateTimeSymbol, dateTimeOffsetSymbol))
return;
}

context.ReportDiagnostic(Rule, type);
}

private static bool IsTimeProviderLikeMember(ISymbol member, INamedTypeSymbol? dateTimeSymbol, INamedTypeSymbol? dateTimeOffsetSymbol)
{
if (member.IsStatic)
return false;

if (member is IPropertySymbol property)
Comment thread
meziantou marked this conversation as resolved.
{
if (property.Name is not ("Now" or "UtcNow" or "GetNow" or "GetUtcNow" or "CurrentTime"))
return false;

return IsDateTimeOrDateTimeOffset(property.Type, dateTimeSymbol, dateTimeOffsetSymbol);
}

if (member is IMethodSymbol method)
{
// Exclude property accessor methods
if (method.MethodKind is not MethodKind.Ordinary)
return false;

if (method.Name is not ("Now" or "UtcNow" or "GetNow" or "GetUtcNow" or "CurrentTime"))
return false;

if (!method.Parameters.IsEmpty)
return false;

return IsDateTimeOrDateTimeOffset(method.ReturnType, dateTimeSymbol, dateTimeOffsetSymbol);
}

return false;
}

private static bool IsDateTimeOrDateTimeOffset(ITypeSymbol type, INamedTypeSymbol? dateTimeSymbol, INamedTypeSymbol? dateTimeOffsetSymbol)
{
return type.IsEqualTo(dateTimeSymbol) || type.IsEqualTo(dateTimeOffsetSymbol);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using Meziantou.Analyzer.Rules;
using TestHelper;

namespace Meziantou.Analyzer.Test.Rules;

public sealed class UseTimeProviderInsteadOfInterfaceAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<UseTimeProviderInsteadOfInterfaceAnalyzer>();
}

[Fact]
public async Task Interface_UtcNowProperty_DateTime_ReportsDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface [|ITimeProvider|]
{
System.DateTime UtcNow { get; }
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_NowProperty_DateTimeOffset_ReportsDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface [|ITimeProvider|]
{
System.DateTimeOffset Now { get; }
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_BothNowAndUtcNow_ReportsDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface [|ITimeProvider|]
{
System.DateTime Now { get; }
System.DateTime UtcNow { get; }
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_GetNowMethod_ReportsDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface [|ITimeProvider|]
{
System.DateTime GetNow();
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_GetUtcNowMethod_ReportsDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface [|ITimeProvider|]
{
System.DateTimeOffset GetUtcNow();
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_MixedProperties_ReportsDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface [|ITimeService|]
{
System.DateTime GetNow();
System.DateTimeOffset GetUtcNow();
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_EmptyInterface_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface IEmpty
{
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_OtherMembers_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface ITimeProvider
{
System.DateTime UtcNow { get; }
void DoSomething();
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_WrongReturnType_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface ITimeProvider
{
string UtcNow { get; }
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_MethodWithParameters_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface ITimeProvider
{
System.DateTime GetNow(string timeZone);
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_CurrentTimeProperty_ReportsDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface [|ITimeProvider|]
{
System.DateTime CurrentTime { get; }
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_WrongName_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface ITimeProvider
{
System.DateTime GetCurrentTime();
}
""")
.ValidateAsync();
}

[Fact]
public async Task Class_NotAnInterface_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
class TimeProvider
{
public System.DateTime UtcNow { get; }
}
""")
.ValidateAsync();
}

[Fact]
public async Task Interface_StaticMember_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
interface ITimeProvider
{
static System.DateTime UtcNow { get; }
}
""")
.ValidateAsync();
}
}