-
-
Notifications
You must be signed in to change notification settings - Fork 63
Add MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0) #929
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
Changes from 2 commits
1206ae7
024bc3d
6849e2d
6cbadef
15fa55a
aa4db9f
74eeeb2
933d6fc
32fab0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # MA0178 - Use TimeSpan.Zero instead of TimeSpan.FromXXX(0) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| using System.Collections.Immutable; | ||
| using System.Composition; | ||
| using Meziantou.Analyzer.Internals; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CodeActions; | ||
| using Microsoft.CodeAnalysis.CodeFixes; | ||
| using Microsoft.CodeAnalysis.Editing; | ||
|
|
||
| namespace Meziantou.Analyzer.Rules; | ||
|
|
||
| [ExportCodeFixProvider(LanguageNames.CSharp), Shared] | ||
| public sealed class UseTimeSpanZeroFixer : CodeFixProvider | ||
| { | ||
| public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseTimeSpanZero); | ||
|
|
||
| public override FixAllProvider GetFixAllProvider() | ||
| { | ||
| return WellKnownFixAllProviders.BatchFixer; | ||
| } | ||
|
|
||
| public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
| { | ||
| var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
| var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); | ||
| if (nodeToFix is null) | ||
| return; | ||
|
|
||
| var title = "Use TimeSpan.Zero"; | ||
| var codeAction = CodeAction.Create( | ||
| title, | ||
| ct => ConvertToTimeSpanZero(context.Document, nodeToFix, ct), | ||
| equivalenceKey: title); | ||
|
|
||
| context.RegisterCodeFix(codeAction, context.Diagnostics); | ||
| } | ||
|
|
||
| private static async Task<Document> ConvertToTimeSpanZero(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) | ||
| { | ||
| var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); | ||
| var semanticModel = editor.SemanticModel; | ||
| var generator = editor.Generator; | ||
|
|
||
| editor.ReplaceNode(nodeToFix, GenerateTimeSpanZeroExpression(generator, semanticModel).WithTriviaFrom(nodeToFix)); | ||
| return editor.GetChangedDocument(); | ||
| } | ||
|
|
||
| private static SyntaxNode GenerateTimeSpanZeroExpression(SyntaxGenerator generator, SemanticModel semanticModel) | ||
| { | ||
| return generator.MemberAccessExpression( | ||
| generator.TypeExpression(semanticModel.Compilation.GetBestTypeByMetadataName("System.TimeSpan")!), | ||
|
||
| nameof(TimeSpan.Zero)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| using System.Collections.Immutable; | ||
| using Meziantou.Analyzer.Internals; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
| using Microsoft.CodeAnalysis.Operations; | ||
|
|
||
| namespace Meziantou.Analyzer.Rules; | ||
|
|
||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public sealed class UseTimeSpanZeroAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| private static readonly DiagnosticDescriptor Rule = new( | ||
| RuleIdentifiers.UseTimeSpanZero, | ||
| title: "Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)", | ||
| messageFormat: "Use TimeSpan.Zero instead of TimeSpan.{0}(0)", | ||
| RuleCategories.Design, | ||
| DiagnosticSeverity.Info, | ||
| isEnabledByDefault: true, | ||
| description: "", | ||
| helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseTimeSpanZero)); | ||
|
|
||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | ||
|
|
||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.EnableConcurrentExecution(); | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
|
|
||
| context.RegisterCompilationStartAction(compilationContext => | ||
| { | ||
| var timeSpanType = compilationContext.Compilation.GetBestTypeByMetadataName("System.TimeSpan"); | ||
| if (timeSpanType is null) | ||
| return; | ||
|
|
||
| compilationContext.RegisterOperationAction(context => AnalyzeInvocationOperation(context, timeSpanType), OperationKind.Invocation); | ||
| }); | ||
| } | ||
|
|
||
| private static void AnalyzeInvocationOperation(OperationAnalysisContext context, INamedTypeSymbol timeSpanType) | ||
| { | ||
| var operation = (IInvocationOperation)context.Operation; | ||
|
|
||
| // Check if the method is a static method on System.TimeSpan | ||
| if (!operation.TargetMethod.IsStatic) | ||
| return; | ||
|
|
||
| if (!operation.TargetMethod.ContainingType.IsEqualTo(timeSpanType)) | ||
| return; | ||
|
|
||
| // Check if it's one of the From methods | ||
| var methodName = operation.TargetMethod.Name; | ||
| if (!IsTimeSpanFromMethod(methodName)) | ||
| return; | ||
|
|
||
| // Check if the method has exactly one argument | ||
| if (operation.Arguments.Length != 1) | ||
| return; | ||
|
|
||
| // Check if the argument is a constant value of 0 | ||
| var argument = operation.Arguments[0]; | ||
| if (!argument.Value.ConstantValue.HasValue) | ||
| return; | ||
|
|
||
| var constantValue = argument.Value.ConstantValue.Value; | ||
| if (!IsZero(constantValue)) | ||
| return; | ||
|
|
||
| context.ReportDiagnostic(Rule, operation, methodName); | ||
| } | ||
|
|
||
| private static bool IsTimeSpanFromMethod(string methodName) | ||
| { | ||
| return methodName is "FromDays" or "FromHours" or "FromMinutes" or "FromSeconds" or "FromMilliseconds" or "FromMicroseconds" or "FromTicks"; | ||
| } | ||
|
|
||
| private static bool IsZero(object? value) | ||
meziantou marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| return value switch | ||
| { | ||
| int i => i == 0, | ||
| long l => l == 0, | ||
| double d => d == 0.0, | ||
| float f => f == 0.0f, | ||
| decimal dec => dec == 0m, | ||
| byte b => b == 0, | ||
| sbyte sb => sb == 0, | ||
| short s => s == 0, | ||
| ushort us => us == 0, | ||
| uint ui => ui == 0, | ||
| ulong ul => ul == 0, | ||
| _ => false, | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| using Meziantou.Analyzer.Rules; | ||
| using TestHelper; | ||
|
|
||
| namespace Meziantou.Analyzer.Test.Rules; | ||
|
|
||
| public sealed class UseTimeSpanZeroAnalyzerTests | ||
| { | ||
| private static ProjectBuilder CreateProjectBuilder() | ||
| { | ||
| return new ProjectBuilder() | ||
| .WithAnalyzer<UseTimeSpanZeroAnalyzer>() | ||
| .WithCodeFixProvider<UseTimeSpanZeroFixer>(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("TimeSpan.FromSeconds(0)")] | ||
| [InlineData("TimeSpan.FromSeconds(0.0)")] | ||
| [InlineData("TimeSpan.FromMinutes(0)")] | ||
| [InlineData("TimeSpan.FromMinutes(0.0)")] | ||
| [InlineData("TimeSpan.FromHours(0)")] | ||
| [InlineData("TimeSpan.FromHours(0.0)")] | ||
| [InlineData("TimeSpan.FromDays(0)")] | ||
| [InlineData("TimeSpan.FromDays(0.0)")] | ||
| [InlineData("TimeSpan.FromMilliseconds(0)")] | ||
| [InlineData("TimeSpan.FromMilliseconds(0.0)")] | ||
| [InlineData("TimeSpan.FromMicroseconds(0)")] | ||
| [InlineData("TimeSpan.FromMicroseconds(0.0)")] | ||
| [InlineData("TimeSpan.FromTicks(0)")] | ||
| [InlineData("TimeSpan.FromTicks(0L)")] | ||
| [InlineData("System.TimeSpan.FromSeconds(0)")] | ||
| public async Task ShouldReportDiagnostic(string code) | ||
| { | ||
| await CreateProjectBuilder() | ||
| .WithSourceCode($$""" | ||
| class TestClass | ||
| { | ||
| void Test() | ||
| { | ||
| _ = [||]{{code}}; | ||
| } | ||
| } | ||
| """) | ||
| .ShouldFixCodeWith(""" | ||
| class TestClass | ||
| { | ||
| void Test() | ||
| { | ||
| _ = System.TimeSpan.Zero; | ||
| } | ||
| } | ||
| """) | ||
| .ValidateAsync(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("TimeSpan.FromSeconds(1)")] | ||
| [InlineData("TimeSpan.FromSeconds(0.5)")] | ||
| [InlineData("TimeSpan.FromMinutes(1)")] | ||
| [InlineData("TimeSpan.FromHours(1)")] | ||
| [InlineData("TimeSpan.FromDays(1)")] | ||
| [InlineData("TimeSpan.FromMilliseconds(100)")] | ||
| [InlineData("TimeSpan.FromMicroseconds(1)")] | ||
| [InlineData("TimeSpan.FromTicks(1)")] | ||
| [InlineData("TimeSpan.Zero")] | ||
| [InlineData("new TimeSpan()")] | ||
| [InlineData("new TimeSpan(0)")] | ||
| [InlineData("default(TimeSpan)")] | ||
| public async Task ShouldNotReportDiagnostic(string code) | ||
| { | ||
| await CreateProjectBuilder() | ||
| .WithSourceCode($$""" | ||
| class TestClass | ||
| { | ||
| void Test() | ||
| { | ||
| _ = {{code}}; | ||
| } | ||
| } | ||
| """) | ||
| .ValidateAsync(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ShouldReportDiagnostic_MultipleOccurrences() | ||
| { | ||
| await CreateProjectBuilder() | ||
| .WithSourceCode(""" | ||
| class TestClass | ||
| { | ||
| void Test() | ||
| { | ||
| _ = [||]TimeSpan.FromSeconds(0); | ||
| _ = [||]TimeSpan.FromMinutes(0); | ||
| _ = [||]TimeSpan.FromHours(0); | ||
| } | ||
| } | ||
| """) | ||
| .ShouldFixCodeWith(""" | ||
| class TestClass | ||
| { | ||
| void Test() | ||
| { | ||
| _ = System.TimeSpan.Zero; | ||
| _ = System.TimeSpan.Zero; | ||
| _ = System.TimeSpan.Zero; | ||
| } | ||
| } | ||
| """) | ||
| .ValidateAsync(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot Can you write the documentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Added comprehensive documentation with code examples showing all non-compliant patterns and the compliant alternative. Commit: 6849e2d