diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/CSharpCollapseMultiplePathOperations.Fixer.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/CSharpCollapseMultiplePathOperations.Fixer.cs new file mode 100644 index 000000000000..72ea0760b201 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/CSharpCollapseMultiplePathOperations.Fixer.cs @@ -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 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 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(); + + 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; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md index e4e79681a2de..71d86f8f226b 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -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. diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif index 5ffed0b65987..0c37feb04484 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -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", diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md index ac199254a8b0..2be498bb6b60 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md @@ -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) diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 0e44aa08e427..3d22f1bfcea5 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -2240,4 +2240,16 @@ Widening and user defined conversions are not supported with generic types. Replace obsolete call + + Collapse consecutive Path.Combine or Path.Join operations + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + Collapse into single Path.{0} operation + diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/CollapseMultiplePathOperations.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/CollapseMultiplePathOperations.cs new file mode 100644 index 000000000000..8a122ca18107 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/CollapseMultiplePathOperations.cs @@ -0,0 +1,307 @@ +// 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 Analyzer.Utilities; +using Analyzer.Utilities.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + using static MicrosoftNetCoreAnalyzersResources; + + /// + /// CA1877: + /// Detects nested Path.Combine or Path.Join calls that can be collapsed into a single call. + /// Example: Path.Combine(Path.Combine(a, b), c) -> Path.Combine(a, b, c) + /// + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class CollapseMultiplePathOperationsAnalyzer : DiagnosticAnalyzer + { + internal const string RuleId = "CA1877"; + internal const string MethodNameKey = "MethodName"; + internal const string ArgumentCountKey = "ArgumentCount"; + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + id: RuleId, + title: CreateLocalizableResourceString(nameof(CollapseMultiplePathOperationsTitle)), + messageFormat: CreateLocalizableResourceString(nameof(CollapseMultiplePathOperationsMessage)), + category: DiagnosticCategory.Performance, + ruleLevel: RuleLevel.IdeSuggestion, + description: CreateLocalizableResourceString(nameof(CollapseMultiplePathOperationsDescription)), + isPortedFxCopRule: false, + isDataflowRule: false); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(compilationContext => + { + var compilation = compilationContext.Compilation; + + // Get the Path type + if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIOPath, out var pathType)) + { + return; + } + + // Get Span types (may be null if not available in the target framework) + compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemSpan1, out var spanType); + compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1, out var readOnlySpanType); + + // Get Combine and Join methods + var combineMethods = ImmutableArray.CreateBuilder(); + var joinMethods = ImmutableArray.CreateBuilder(); + + foreach (var member in pathType.GetMembers()) + { + if (member is IMethodSymbol method && method.IsStatic) + { + if (method.Name == "Combine" && IsStringReturningMethod(method)) + { + combineMethods.Add(method); + } + else if (method.Name == "Join" && IsStringReturningMethod(method)) + { + joinMethods.Add(method); + } + } + } + + if (combineMethods.Count == 0 && joinMethods.Count == 0) + { + return; + } + + var combineMethodsArray = combineMethods.ToImmutable(); + var joinMethodsArray = joinMethods.ToImmutable(); + + compilationContext.RegisterOperationAction(operationContext => + { + var invocation = (IInvocationOperation)operationContext.Operation; + AnalyzeInvocation(operationContext, invocation, pathType, spanType, readOnlySpanType, combineMethodsArray, joinMethodsArray); + }, OperationKind.Invocation); + }); + } + + private static bool IsStringReturningMethod(IMethodSymbol method) + { + return method.ReturnType.SpecialType == SpecialType.System_String; + } + + private static void AnalyzeInvocation( + OperationAnalysisContext context, + IInvocationOperation invocation, + INamedTypeSymbol pathType, + INamedTypeSymbol? spanType, + INamedTypeSymbol? readOnlySpanType, + ImmutableArray combineMethods, + ImmutableArray joinMethods) + { + var targetMethod = invocation.TargetMethod; + + // Early check: must have arguments, be a static method, and be on System.IO.Path + if (invocation.Arguments.IsEmpty || + !targetMethod.IsStatic || + !SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, pathType)) + { + return; + } + + string? methodName = null; + ImmutableArray methodsToCheck = default; + + // Check if this is a Combine or Join call + foreach (var method in combineMethods) + { + if (SymbolEqualityComparer.Default.Equals(targetMethod.OriginalDefinition, method.OriginalDefinition)) + { + methodName = "Combine"; + methodsToCheck = combineMethods; + break; + } + } + + if (methodName == null) + { + foreach (var method in joinMethods) + { + if (SymbolEqualityComparer.Default.Equals(targetMethod.OriginalDefinition, method.OriginalDefinition)) + { + methodName = "Join"; + methodsToCheck = joinMethods; + break; + } + } + } + + if (methodName == null) + { + return; + } + + // Check if this invocation is itself an argument to another Path.Combine/Join call + // If so, skip it - we'll report on the outermost call only + if (IsNestedInSimilarCall(invocation, methodsToCheck)) + { + return; + } + + // Check if any argument is itself a Path.Combine/Join call of the same method + foreach (var argument in invocation.Arguments) + { + if (argument.Value is IInvocationOperation nestedInvocation) + { + var nestedMethod = nestedInvocation.TargetMethod; + + foreach (var method in methodsToCheck) + { + if (SymbolEqualityComparer.Default.Equals(nestedMethod.OriginalDefinition, method.OriginalDefinition)) + { + // Found a nested call that can potentially be collapsed + // Count total arguments to ensure we don't exceed available overloads + int totalArgs = CountTotalArguments(invocation, methodsToCheck); + + // For Combine and Join with string parameters, we can use params overload for any count + // Check if target framework has the overloads we need + if (CanCollapse(invocation, spanType, readOnlySpanType, totalArgs)) + { + var properties = ImmutableDictionary.CreateBuilder(); + properties.Add(MethodNameKey, methodName); + properties.Add(ArgumentCountKey, totalArgs.ToString()); + + context.ReportDiagnostic(invocation.CreateDiagnostic(Rule, properties.ToImmutable(), methodName)); + return; + } + } + } + } + } + } + + private static bool IsNestedInSimilarCall(IInvocationOperation invocation, ImmutableArray methodsToCheck) + { + // Walk up the tree to see if this invocation is an argument to another Path.Combine/Join call + var current = invocation.Parent; + while (current != null) + { + // Check if we're inside an argument + if (current is IArgumentOperation) + { + // Get the invocation that contains this argument + var grandParent = current.Parent; + if (grandParent is IInvocationOperation parentInvocation) + { + var parentMethod = parentInvocation.TargetMethod; + foreach (var method in methodsToCheck) + { + if (SymbolEqualityComparer.Default.Equals(parentMethod.OriginalDefinition, method.OriginalDefinition)) + { + return true; + } + } + } + } + current = current.Parent; + } + return false; + } + + private static int CountTotalArguments(IInvocationOperation invocation, ImmutableArray methodsToCheck) + { + int count = 0; + + foreach (var argument in invocation.Arguments) + { + if (argument.Value is IInvocationOperation nestedInvocation) + { + var nestedMethod = nestedInvocation.TargetMethod; + bool isNestedPathMethod = false; + + foreach (var method in methodsToCheck) + { + if (SymbolEqualityComparer.Default.Equals(nestedMethod.OriginalDefinition, method.OriginalDefinition)) + { + isNestedPathMethod = true; + break; + } + } + + if (isNestedPathMethod) + { + // Recursively count arguments from nested call + count += CountTotalArguments(nestedInvocation, methodsToCheck); + } + else + { + count++; + } + } + else + { + count++; + } + } + + return count; + } + + private static bool CanCollapse(IInvocationOperation invocation, INamedTypeSymbol? spanType, INamedTypeSymbol? readOnlySpanType, int totalArgs) + { + // We can collapse if there's a params overload available + // Path.Combine(params string[]) and Path.Join(params string[]) exist in supported frameworks + // The only constraint is that we need at least 2 arguments total + + // However, if any of the parameters are spans, we can't use the params overload + // Check if any argument involves span types + if (HasSpanArguments(invocation, spanType, readOnlySpanType)) + { + // With span arguments, we're limited to the non-params overloads + // which support up to 4 arguments for Join(ReadOnlySpan) + return totalArgs >= 2 && totalArgs <= 4; + } + + return totalArgs >= 2; + } + + private static bool HasSpanArguments(IInvocationOperation invocation, INamedTypeSymbol? spanType, INamedTypeSymbol? readOnlySpanType) + { + foreach (var argument in invocation.Arguments) + { + if (IsSpanType(argument.Value.Type, spanType, readOnlySpanType)) + { + return true; + } + + // Check nested invocations + if (argument.Value is IInvocationOperation nestedInvocation) + { + if (HasSpanArguments(nestedInvocation, spanType, readOnlySpanType)) + { + return true; + } + } + } + + return false; + } + + private static bool IsSpanType(ITypeSymbol? type, INamedTypeSymbol? spanType, INamedTypeSymbol? readOnlySpanType) + { + if (type is not INamedTypeSymbol namedType) + { + return false; + } + + // Use symbol comparison with the span types looked up from compilation + var originalDefinition = namedType.OriginalDefinition; + return (spanType != null && SymbolEqualityComparer.Default.Equals(originalDefinition, spanType)) || + (readOnlySpanType != null && SymbolEqualityComparer.Default.Equals(originalDefinition, readOnlySpanType)); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf index b4ae87adecd3..b7c4568b62f2 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -237,6 +237,26 @@ Metody Dispose by měly volat SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf index 9378929c6872..688ea4045370 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -237,6 +237,26 @@ Dispose-Methoden müssen SuppressFinalize aufrufen + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf index 80bbd7a64c91..aba5ba077e58 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -237,6 +237,26 @@ Los métodos Dispose deberían llamar a SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf index dd029e6a6c95..7f5f43118ef4 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -237,6 +237,26 @@ Les méthodes Dispose doivent appeler SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf index 1246482b3498..7bc04e7cf16b 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -237,6 +237,26 @@ I metodi Dispose devono chiamare SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf index 4776a19ad06d..2a0c3b6449af 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -237,6 +237,26 @@ Dispose メソッドは、SuppressFinalize を呼び出す必要があります + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf index a06d012d783e..415f3a7e2a99 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -237,6 +237,26 @@ Dispose 메서드는 SuppressFinalize를 호출해야 합니다. + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf index 587148c66fda..ad785b849f19 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -237,6 +237,26 @@ Metoda Dispose powinna wywoływać metodę SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf index 1fea153d3761..34f08d4cab3f 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf @@ -237,6 +237,26 @@ Os métodos Dispose devem chamar SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf index 8a880cded702..3cbf16c455e5 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -237,6 +237,26 @@ Методы Dispose должны вызывать SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf index 9ed97874548b..83d53bd4094c 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -237,6 +237,26 @@ Dispose yöntemleri tarafından SuppressFinalize çağrılmalıdır + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf index 732103d6a0da..3ea52ef16167 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf @@ -237,6 +237,26 @@ Dispose 方法应调用 SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf index 9917a4f5fb3f..8ccaca100c65 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf @@ -237,6 +237,26 @@ Dispose 方法應該呼叫 SuppressFinalize + + Collapse into single Path.{0} operation + Collapse into single Path.{0} operation + + + + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability. + + + + Multiple consecutive Path.{0} operations can be collapsed into a single operation + Multiple consecutive Path.{0} operations can be collapsed into a single operation + + + + Collapse consecutive Path.Combine or Path.Join operations + Collapse consecutive Path.Combine or Path.Join operations + + , , diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/RulesMissingDocumentation.md b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/RulesMissingDocumentation.md index c5820fde91ae..75e0f77588e2 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/RulesMissingDocumentation.md +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/RulesMissingDocumentation.md @@ -2,7 +2,3 @@ Rule ID | Missing Help Link | Title | --------|-------------------|-------| -CA1873 | | Avoid potentially expensive logging | -CA1874 | | Use 'Regex.IsMatch' | -CA1875 | | Use 'Regex.Count' | -CA2023 | | Invalid braces in message template | diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index bceb136a39cc..03683916f277 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -12,7 +12,7 @@ Design: CA2210, CA1000-CA1070 Globalization: CA2101, CA1300-CA1311 Mobility: CA1600-CA1601 -Performance: HA, CA1800-CA1876 +Performance: HA, CA1800-CA1877 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5405 Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2265 Naming: CA1700-CA1727 diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Performance/CollapseMultiplePathOperationsTests.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Performance/CollapseMultiplePathOperationsTests.cs new file mode 100644 index 000000000000..fc3b1a4301b5 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Performance/CollapseMultiplePathOperationsTests.cs @@ -0,0 +1,401 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< + Microsoft.NetCore.Analyzers.Performance.CollapseMultiplePathOperationsAnalyzer, + Microsoft.NetCore.CSharp.Analyzers.Performance.CSharpCollapseMultiplePathOperationsFixer>; + +namespace Microsoft.NetCore.Analyzers.Performance.UnitTests +{ + public class CollapseMultiplePathOperationsTests + { + [Fact] + public async Task NoDiagnostic_SingleCombineCall() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b"); + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(csCode); + } + + [Fact] + public async Task NoDiagnostic_SingleJoinCall() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Join("a", "b"); + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(csCode); + } + + [Fact] + public async Task Diagnostic_NestedCombineCalls() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Combine(Path.Combine("a", "b"), "c")|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b", "c"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_NestedJoinCalls() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Join(Path.Join("a", "b"), "c")|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Join("a", "b", "c"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_DeeplyNestedCombineCalls() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Combine(Path.Combine(Path.Combine("a", "b"), "c"), "d")|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b", "c", "d"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_FullyQualifiedName() + { + var csCode = """ + public class Test + { + public void M() + { + string path = [|System.IO.Path.Combine(System.IO.Path.Combine("a", "b"), "c")|]; + } + } + """; + var fixedCode = """ + public class Test + { + public void M() + { + string path = System.IO.Path.Combine("a", "b", "c"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task NoDiagnostic_DifferentMethods() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine(Path.Join("a", "b"), "c"); + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(csCode); + } + + [Fact] + public async Task Diagnostic_WithMultipleArgumentsInInnerCall() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Combine(Path.Combine("a", "b", "c"), "d")|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b", "c", "d"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_MultipleNestedLevels() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Combine(Path.Combine("a", "b"), Path.Combine("c", "d"))|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b", "c", "d"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_LargeNumberOfArguments() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Combine( + Path.Combine( + Path.Combine("a", "b"), + Path.Combine("c", "d")), + Path.Combine( + Path.Combine("e", "f"), + Path.Combine("g", "h")))|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b", "c", "d", "e", "f", "g", "h"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_NestedInMiddlePosition() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Combine("a", Path.Combine("b", "c"), "d")|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b", "c", "d"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_NestedInLastPosition() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = [|Path.Combine("a", "b", Path.Combine("c", "d"))|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine("a", "b", "c", "d"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task Diagnostic_WithNonConstArguments() + { + var csCode = """ + using System.IO; + + public class Test + { + private string GetPath() => "path"; + private string MyProperty => "prop"; + + public void M(bool condition) + { + string path = [|Path.Combine(Path.Combine(GetPath(), MyProperty), condition ? "a" : "b")|]; + } + } + """; + var fixedCode = """ + using System.IO; + + public class Test + { + private string GetPath() => "path"; + private string MyProperty => "prop"; + + public void M(bool condition) + { + string path = Path.Combine(GetPath(), MyProperty, condition ? "a" : "b"); + } + } + """; + await VerifyCS.VerifyCodeFixAsync(csCode, fixedCode); + } + + [Fact] + public async Task NoDiagnostic_JoinNestedInCombine() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Join(Path.Combine("a", "b"), "c"); + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(csCode); + } + + [Fact] + public async Task NoDiagnostic_CombineNestedInJoinMultipleLevels() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine(Path.Join(Path.Combine("a", "b"), "c"), "d"); + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(csCode); + } + + [Fact] + public async Task NoDiagnostic_DeeplyMixedCombineAndJoinNesting() + { + var csCode = """ + using System.IO; + + public class Test + { + public void M() + { + string path = Path.Combine(Path.Join(Path.Combine(Path.Combine("a", "b"), "c"), "d"), "e"); + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(csCode); + } + } +}