-
-
Notifications
You must be signed in to change notification settings - Fork 65
Add MA0188: Use System.TimeProvider instead of a custom time abstraction #1055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
516479e
Initial plan
Copilot ffa5bcd
Add MA0188: Use System.TimeProvider instead of a custom time abstraction
Copilot 2862e78
MA0188: Expand name list, remove redundant check, add CurrentTime test
Copilot 3d607ee
MA0188: Mention Microsoft.Bcl.TimeProvider backport for older TFMs in…
Copilot 05c0474
MA0188: Enable rule by default
Copilot 03334ef
MA0188: Check member is not static; simplify IsDateTimeOrDateTimeOffs…
Copilot 85622f6
MA0188: Remove configuration section from docs
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # MA0188 - Use System.TimeProvider instead of a custom time abstraction | ||
| <!-- 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(); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
src/Meziantou.Analyzer/Rules/UseTimeProviderInsteadOfInterfaceAnalyzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
|
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); | ||
| } | ||
| } | ||
197 changes: 197 additions & 0 deletions
197
tests/Meziantou.Analyzer.Test/Rules/UseTimeProviderInsteadOfInterfaceAnalyzerTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.