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
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Composition;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.NetCore.Analyzers;
using Microsoft.NetCore.Analyzers.Performance;

namespace Microsoft.NetCore.CSharp.Analyzers.Performance
{
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class CSharpCollapseMultiplePathOperationsFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(CollapseMultiplePathOperationsAnalyzer.RuleId);

public override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var document = context.Document;
var diagnostic = context.Diagnostics[0];
var root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);

if (node is not InvocationExpressionSyntax invocation ||
await document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is not { } semanticModel ||
semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemIOPath) is not { } pathType)
{
return;
}

// Get the method name from diagnostic properties
if (!diagnostic.Properties.TryGetValue(CollapseMultiplePathOperationsAnalyzer.MethodNameKey, out var methodName))
{
methodName = "Path";
}

context.RegisterCodeFix(
CodeAction.Create(
string.Format(MicrosoftNetCoreAnalyzersResources.CollapseMultiplePathOperationsCodeFixTitle, methodName),
createChangedDocument: cancellationToken => CollapsePathOperationAsync(document, root, invocation, pathType, semanticModel, cancellationToken),
equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.CollapseMultiplePathOperationsCodeFixTitle)),
diagnostic);
}

private static Task<Document> CollapsePathOperationAsync(Document document, SyntaxNode root, InvocationExpressionSyntax invocation, INamedTypeSymbol pathType, SemanticModel semanticModel, CancellationToken cancellationToken)
{
// Collect all arguments by recursively unwrapping nested Path.Combine/Join calls
var allArguments = CollectAllArguments(invocation, pathType, semanticModel);

// Create new argument list with all collected arguments
var newArgumentList = SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList(allArguments));

// Create the new invocation with all arguments
var newInvocation = invocation.WithArgumentList(newArgumentList)
.WithTriviaFrom(invocation);

var newRoot = root.ReplaceNode(invocation, newInvocation);

return Task.FromResult(document.WithSyntaxRoot(newRoot));
}

private static ArgumentSyntax[] CollectAllArguments(InvocationExpressionSyntax invocation, INamedTypeSymbol pathType, SemanticModel semanticModel)
{
var arguments = ImmutableArray.CreateBuilder<ArgumentSyntax>();

foreach (var argument in invocation.ArgumentList.Arguments)
{
if (argument.Expression is InvocationExpressionSyntax nestedInvocation &&
IsPathCombineOrJoin(nestedInvocation, pathType, semanticModel, out var methodName) &&
IsPathCombineOrJoin(invocation, pathType, semanticModel, out var outerMethodName) &&
methodName == outerMethodName)
{
// Recursively collect arguments from nested invocation
arguments.AddRange(CollectAllArguments(nestedInvocation, pathType, semanticModel));
}
else
{
arguments.Add(argument);
}
}

return arguments.ToArray();
}

private static bool IsPathCombineOrJoin(InvocationExpressionSyntax invocation, INamedTypeSymbol pathType, SemanticModel semanticModel, out string methodName)
{
if (semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol methodSymbol &&
SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, pathType) &&
methodSymbol.Name is "Combine" or "Join")
{
methodName = methodSymbol.Name;
return true;
}

methodName = string.Empty;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1920,6 +1920,18 @@ Using 'AsParallel()' directly in a 'foreach' loop has no effect. The 'foreach' s
|CodeFix|False|
---

## [CA1877](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1877): Collapse consecutive Path.Combine or Path.Join operations

When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability.

|Item|Value|
|-|-|
|Category|Performance|
|Enabled|True|
|Severity|Info|
|CodeFix|True|
---

## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope

If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3527,6 +3527,26 @@
]
}
},
"CA1877": {
"id": "CA1877",
"shortDescription": "Collapse consecutive Path.Combine or Path.Join operations",
"fullDescription": "When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability.",
"defaultLevel": "note",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1877",
"properties": {
"category": "Performance",
"isEnabledByDefault": true,
"typeName": "CollapseMultiplePathOperationsAnalyzer",
"languages": [
"C#",
"Visual Basic"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2000": {
"id": "CA2000",
"shortDescription": "Dispose objects before losing scope",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ CA1873 | Performance | Info | AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer,
CA1874 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1874)
CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)
CA1876 | Performance | Info | DoNotUseAsParallelInForEachLoopAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876)
CA1877 | Performance | Info | CollapseMultiplePathOperationsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/cA1877)
CA2023 | Reliability | Warning | LoggerMessageDefineAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2023)
CA2024 | Reliability | Warning | DoNotUseEndOfStreamInAsyncMethods, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
CA2025 | Reliability | Disabled | DoNotPassDisposablesIntoUnawaitedTasksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2025)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2240,4 +2240,16 @@ Widening and user defined conversions are not supported with generic types.</val
<data name="DoNotUseThreadVolatileReadWriteCodeFixTitle" xml:space="preserve">
<value>Replace obsolete call</value>
</data>
<data name="CollapseMultiplePathOperationsTitle" xml:space="preserve">
<value>Collapse consecutive Path.Combine or Path.Join operations</value>
</data>
<data name="CollapseMultiplePathOperationsMessage" xml:space="preserve">
<value>Multiple consecutive Path.{0} operations can be collapsed into a single operation</value>
</data>
<data name="CollapseMultiplePathOperationsDescription" xml:space="preserve">
<value>When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability.</value>
</data>
<data name="CollapseMultiplePathOperationsCodeFixTitle" xml:space="preserve">
<value>Collapse into single Path.{0} operation</value>
</data>
</root>
Loading
Loading