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 @@ -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
```
20 changes: 20 additions & 0 deletions docs/Rules/MA0178.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# MA0178 - Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)
<!-- sources -->
Sources: [UseTimeSpanZeroAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseTimeSpanZeroAnalyzer.cs), [UseTimeSpanZeroFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseTimeSpanZeroFixer.cs)
<!-- sources -->

You should use `TimeSpan.Zero` instead of `TimeSpan.FromXXX(0)` for better readability and to express the intent more clearly.

````csharp
TimeSpan.FromSeconds(0); // non-compliant
TimeSpan.FromMinutes(0); // non-compliant
TimeSpan.FromHours(0); // non-compliant
TimeSpan.FromDays(0); // non-compliant
TimeSpan.FromMilliseconds(0); // non-compliant
TimeSpan.FromMicroseconds(0); // non-compliant
TimeSpan.FromTicks(0); // non-compliant

// Should be
TimeSpan.Zero; // compliant
````

62 changes: 62 additions & 0 deletions src/Meziantou.Analyzer.CodeFixers/Rules/UseTimeSpanZeroFixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel is null)
return;

var timeSpanType = semanticModel.Compilation.GetBestTypeByMetadataName("System.TimeSpan");
if (timeSpanType 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)
{
var timeSpanType = semanticModel.Compilation.GetBestTypeByMetadataName("System.TimeSpan");
return generator.MemberAccessExpression(
generator.TypeExpression(timeSpanType!),
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
23 changes: 23 additions & 0 deletions src/Meziantou.Analyzer/Internals/NumericHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Meziantou.Analyzer.Internals;

internal static class NumericHelpers
{
public 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,
};
}
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
var members = symbol.GetMembers()
.OfType<IFieldSymbol>()
.Where(member => member.ConstantValue is not null)
.Select(member => (member, IsSingleBitSet: IsSingleBitSet(member.ConstantValue), IsZero: IsZero(member.ConstantValue)))
.Select(member => (member, IsSingleBitSet: IsSingleBitSet(member.ConstantValue), IsZero: NumericHelpers.IsZero(member.ConstantValue)))
.ToArray();
foreach (var member in members)
{
Expand All @@ -66,7 +66,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
}
}

if (!IsZero(value))
if (!NumericHelpers.IsZero(value))
{
context.ReportDiagnostic(Rule, symbol, member.member.Name);
return;
Expand All @@ -93,24 +93,7 @@ private static bool IsSingleBitSet(object? o)
};
}

private static bool IsZero(object? o)
{
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum?WT.mc_id=DT-MVP-5003978
// The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong.
return o switch
{
null => false,
byte x => x == 0,
sbyte x => x == 0,
short x => x == 0,
ushort x => x == 0,
int x => x == 0,
uint x => x == 0,
long x => x == 0,
ulong x => x == 0,
_ => throw new ArgumentOutOfRangeException(nameof(o), $"Type {o.GetType().FullName} is not supported"),
};
}


private static bool IsAllBitsSet(object? o)
{
Expand Down
77 changes: 77 additions & 0 deletions src/Meziantou.Analyzer/Rules/UseTimeSpanZeroAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 == 0)
return;

// Check if the argument is a constant value of 0
foreach (var argument in operation.Arguments)
{
if (!argument.Value.ConstantValue.HasValue)
return;

var constantValue = argument.Value.ConstantValue.Value;
if (!NumericHelpers.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";
}
}
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;
using Xunit;

namespace Meziantou.Analyzer.Test.Rules;

public sealed class UseTimeSpanZeroAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<UseTimeSpanZeroAnalyzer>()
.WithCodeFixProvider<UseTimeSpanZeroFixer>()
.WithTargetFramework(Helpers.TargetFramework.NetLatest);
}

[Theory]
[InlineData("System.TimeSpan.FromSeconds(0)")]
[InlineData("System.TimeSpan.FromSeconds(0.0)")]
[InlineData("System.TimeSpan.FromMinutes(0)")]
[InlineData("System.TimeSpan.FromMinutes(0.0)")]
[InlineData("System.TimeSpan.FromHours(0)")]
[InlineData("System.TimeSpan.FromHours(0.0)")]
[InlineData("System.TimeSpan.FromDays(0)")]
[InlineData("System.TimeSpan.FromDays(0.0)")]
[InlineData("System.TimeSpan.FromMilliseconds(0)")]
[InlineData("System.TimeSpan.FromMilliseconds(0L)")]
[InlineData("System.TimeSpan.FromMilliseconds(0L, 0L)")]
[InlineData("System.TimeSpan.FromMilliseconds(0.0)")]
[InlineData("System.TimeSpan.FromMilliseconds(0.0d)")]
[InlineData("System.TimeSpan.FromMicroseconds(0)")]
[InlineData("System.TimeSpan.FromMicroseconds(0.0)")]
[InlineData("System.TimeSpan.FromTicks(0)")]
[InlineData("System.TimeSpan.FromTicks(0L)")]
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("System.TimeSpan.FromSeconds(1)")]
[InlineData("System.TimeSpan.FromSeconds(0.5)")]
[InlineData("System.TimeSpan.FromMinutes(1)")]
[InlineData("System.TimeSpan.FromHours(1)")]
[InlineData("System.TimeSpan.FromDays(1)")]
[InlineData("System.TimeSpan.FromMilliseconds(100)")]
[InlineData("System.TimeSpan.FromMicroseconds(1)")]
[InlineData("System.TimeSpan.FromTicks(1)")]
[InlineData("System.TimeSpan.Zero")]
[InlineData("new System.TimeSpan()")]
[InlineData("new System.TimeSpan(0)")]
[InlineData("default(System.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()
{
_ = [||]System.TimeSpan.FromSeconds(0);
}
}
""")
.ShouldFixCodeWith("""
class TestClass
{
void Test()
{
_ = System.TimeSpan.Zero;
}
}
""")
.ValidateAsync();
}
}