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 @@ -191,6 +191,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|❌|
|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|❌|
|[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|ℹ️|✔️|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|<span title='Info'>ℹ️</span>|✔️|❌|
|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|<span title='Info'>ℹ️</span>|❌|❌|
|[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>|✔️|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -708,6 +709,9 @@ dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.severity = suggestion
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1234,4 +1238,7 @@ dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.severity = none
```
10 changes: 10 additions & 0 deletions docs/Rules/MA0176.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# MA0176 - Optimize guid creation

Use the constructor overload of `Guid` that takes the individual components of a GUID instead of using the string-based constructor or `Guid.Parse` method.

````c#
_ = new Guid("d3c4d2f1-5f6a-4b7c-8e9f-0a1b2c3d4e5f"); // non-compliant
_ = Guid.Parse("d3c4d2f1-5f6a-4b7c-8e9f-0a1b2c3d4e5f"); // non-compliant

_ = new Guid(0xd3c4d2f1, 0x5f6a, 0x4b7c, 0x8e, 0x9f, 0x0a, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f); // compliant
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading;
using System.Threading.Tasks;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class OptimizeGuidCreationFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.OptimizeGuidCreation);

public override FixAllProvider GetFixAllProvider() => 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 semanticMode = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticMode is null)
return;

var operation = semanticMode.GetOperation(nodeToFix, context.CancellationToken);
string? guidString = null;
if (operation is IObjectCreationOperation creation)
{
guidString = creation.Arguments.FirstOrDefault()?.Value.ConstantValue.Value as string;
}
else if (operation is IInvocationOperation invocation)
{
guidString = invocation.Arguments.FirstOrDefault()?.Value.ConstantValue.Value as string;
}

if (guidString is null || !Guid.TryParse(guidString, out var guid))
return;


var title = "Optimize guid creation";
var codeAction = CodeAction.Create(
title,
ct => Update(context.Document, operation!, guid, guidString, ct),
equivalenceKey: title);
context.RegisterCodeFix(codeAction, context.Diagnostics);
}

private static async Task<Document> Update(Document document, IOperation nodeToFix, Guid guid, string rawGuid, CancellationToken cancellationToken)
{
var parts = guid.ToByteArray();
var data1 = BitConverter.ToInt32(parts, 0);
var data2 = BitConverter.ToInt16(parts, 4);
var data3 = BitConverter.ToInt16(parts, 6);
var data4 = new byte[8];
Array.Copy(parts, 8, data4, 0, 8);

var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var generator = editor.Generator;
var uppercase = rawGuid.All(c => !char.IsLetter(c) || char.IsUpper(c));
var newExpression = generator.ObjectCreationExpression(editor.SemanticModel.Compilation.GetBestTypeByMetadataName("System.Guid")!,
[
CreateHexLiteral(data1, uppercase),
CreateHexLiteral(data2, uppercase),
CreateHexLiteral(data3, uppercase),
CreateHexLiteral(data4[0], uppercase),
CreateHexLiteral(data4[1], uppercase),
CreateHexLiteral(data4[2], uppercase),
CreateHexLiteral(data4[3], uppercase),
CreateHexLiteral(data4[4], uppercase),
CreateHexLiteral(data4[5], uppercase),
CreateHexLiteral(data4[6], uppercase),
CreateHexLiteral(data4[7], uppercase),
])
.WithTrailingTrivia(SyntaxFactory.Comment($" /* {rawGuid} */"));

editor.ReplaceNode(nodeToFix.Syntax, newExpression);
return editor.GetChangedDocument();
}

private static LiteralExpressionSyntax CreateHexLiteral(int value, bool uppercase) => SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal("0x" + value.ToString(uppercase ? "X" : "x", CultureInfo.InvariantCulture), value));
private static LiteralExpressionSyntax CreateHexLiteral(short value, bool uppercase) => SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal("0x" + value.ToString(uppercase ? "X" : "x", CultureInfo.InvariantCulture), value));
private static LiteralExpressionSyntax CreateHexLiteral(byte value, bool uppercase) => SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal("0x" + value.ToString(uppercase ? "X" : "x", CultureInfo.InvariantCulture), value));
}
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,6 @@ dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.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 @@ -523,3 +523,6 @@ dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.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 UseLazyInitializerEnsureInitialize = "MA0173";
public const string RecordClassDeclarationShouldBeExplicit = "MA0174";
public const string RecordClassDeclarationShouldBeImplicit = "MA0175";
public const string OptimizeGuidCreation = "MA0176";

public static string GetHelpUri(string identifier)
{
Expand Down
72 changes: 72 additions & 0 deletions src/Meziantou.Analyzer/Rules/OptimizeGuidCreationAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 OptimizeGuidCreationAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.OptimizeGuidCreation,
title: "Optimize guid creation",
messageFormat: "Optimize guid creation",
RuleCategories.Performance,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.OptimizeGuidCreation));

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

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

context.RegisterCompilationStartAction(ctx =>
{
var type = ctx.Compilation.GetBestTypeByMetadataName("System.Guid");
if (type is null)
return;

ctx.RegisterOperationAction(symbolContext => AnalyzeInvocation(symbolContext, type), OperationKind.Invocation); // Guid.Parse(""), Guid.TryParse("")
ctx.RegisterOperationAction(symbolContext => AnalyzeObjectCreation(symbolContext, type), OperationKind.ObjectCreation); // new Guid("")
});
}

private static void AnalyzeObjectCreation(OperationAnalysisContext symbolContext, INamedTypeSymbol type)
{
var creation = (IObjectCreationOperation)symbolContext.Operation;
if (creation.Constructor is null)
return;

if (!creation.Constructor.ContainingType.IsEqualTo(type))
return;

if (creation is { Arguments: [{ Value.Type.SpecialType: SpecialType.System_String, Value.ConstantValue: { HasValue: true, Value: string value } }] })
{
if (Guid.TryParse(value, out _))
{
symbolContext.ReportDiagnostic(Rule, creation);
}
}
}

private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol guidType)
{
var invocation = (IInvocationOperation)context.Operation;
if (!invocation.TargetMethod.ContainingType.IsEqualTo(guidType))
return;

if (invocation is { TargetMethod.Name: "Parse", Arguments: [{ Value.Type.SpecialType: SpecialType.System_String, Value.ConstantValue: { HasValue: true, Value: string value } }] })
{
if (Guid.TryParse(value, out _))
{
context.ReportDiagnostic(Rule, invocation);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Meziantou.Analyzer.Rules;
using TestHelper;
using Xunit;

namespace Meziantou.Analyzer.Test.Rules;

public sealed class OptimizeGuidParsingAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<OptimizeGuidCreationAnalyzer>()
.WithCodeFixProvider<OptimizeGuidCreationFixer>()
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication);
}

[Fact]
public async Task CtorConstantString()
{
await CreateProjectBuilder()
.WithSourceCode("""
_ = [|new System.Guid("10752bc4-c151-50f5-f27b-df92d8af5a61")|];
""")
.ShouldFixCodeWith("""
_ = new System.Guid(0x10752bc4, 0xc151, 0x50f5, 0xf2, 0x7b, 0xdf, 0x92, 0xd8, 0xaf, 0x5a, 0x61) /* 10752bc4-c151-50f5-f27b-df92d8af5a61 */;
""")
.ValidateAsync();
}

[Fact]
public async Task ParseConstantString()
{
await CreateProjectBuilder()
.WithSourceCode("""
_ = [|System.Guid.Parse("10752BC4-C151-50F5-F27B-DF92D8AF5A61")|];
""")
.ShouldFixCodeWith("""
_ = new System.Guid(0x10752BC4, 0xC151, 0x50F5, 0xF2, 0x7B, 0xDF, 0x92, 0xD8, 0xAF, 0x5A, 0x61) /* 10752BC4-C151-50F5-F27B-DF92D8AF5A61 */;
""")
.ValidateAsync();
}

[Fact]
public async Task ParseNonConstantString()
{
await CreateProjectBuilder()
.WithSourceCode("""
var value = "10752BC4-C151-50F5-F27B-DF92D8AF5A61";
_ = System.Guid.Parse(value);
""")
.ValidateAsync();
}

[Fact]
public async Task ParseInvalidGuid()
{
await CreateProjectBuilder()
.WithSourceCode("""
_ = System.Guid.Parse("dummy");
""")
.ValidateAsync();
}
}