Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -193,6 +193,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|ℹ️|❌|❌|
|[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|ℹ️|✔️|✔️|
|[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|ℹ️|❌|✔️|
|[MA0178](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0178.md)|Design|Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)|ℹ️|✔️|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|<span title='Info'>ℹ️</span>|❌|❌|
|[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0178](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0178.md)|Design|Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)|<span title='Info'>ℹ️</span>|✔️|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -716,6 +717,9 @@ dotnet_diagnostic.MA0176.severity = suggestion

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.severity = none

# MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)
dotnet_diagnostic.MA0178.severity = suggestion
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1248,4 +1252,7 @@ dotnet_diagnostic.MA0176.severity = none

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.severity = none

# MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)
dotnet_diagnostic.MA0178.severity = none
```
1 change: 1 addition & 0 deletions docs/Rules/MA0178.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# MA0178 - Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)
Copy link
Owner

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?

Copy link
Contributor Author

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

53 changes: 53 additions & 0 deletions src/Meziantou.Analyzer.CodeFixers/Rules/UseTimeSpanZeroFixer.cs
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")!),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Can you ensure the type System.TimeSpan exists before suggesting the fix (before CodeAction.Create)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation to check System.TimeSpan type exists before registering the code fix, following the pattern from MarkAttributesWithAttributeUsageAttributeFixer. Commit: 933d6fc

nameof(TimeSpan.Zero));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,6 @@ dotnet_diagnostic.MA0176.severity = suggestion

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.severity = none

# MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)
dotnet_diagnostic.MA0178.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 @@ -529,3 +529,6 @@ dotnet_diagnostic.MA0176.severity = none

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.severity = none

# MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)
dotnet_diagnostic.MA0178.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 @@ -178,6 +178,7 @@ internal static class RuleIdentifiers
public const string RecordClassDeclarationShouldBeImplicit = "MA0175";
public const string OptimizeGuidCreation = "MA0176";
public const string UseSingleLineXmlCommentSyntaxWhenPossible = "MA0177";
public const string UseTimeSpanZero = "MA0178";

public static string GetHelpUri(string identifier)
{
Expand Down
94 changes: 94 additions & 0 deletions src/Meziantou.Analyzer/Rules/UseTimeSpanZeroAnalyzer.cs
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)
{
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,
};
}
}
111 changes: 111 additions & 0 deletions tests/Meziantou.Analyzer.Test/Rules/UseTimeSpanZeroAnalyzerTests.cs
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();
}
}