Skip to content

Commit 4ea0a91

Browse files
authored
Merge branch 'main' into dnup-release-flow
2 parents c61410f + 0394829 commit 4ea0a91

27 files changed

+2455
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
3+
using System.Collections.Immutable;
4+
using System.Composition;
5+
using Analyzer.Utilities;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeActions;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
using Microsoft.NetCore.Analyzers;
12+
using Microsoft.NetCore.Analyzers.Performance;
13+
14+
namespace Microsoft.NetCore.CSharp.Analyzers.Performance
15+
{
16+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
17+
public sealed class CSharpCollapseMultiplePathOperationsFixer : CodeFixProvider
18+
{
19+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(CollapseMultiplePathOperationsAnalyzer.RuleId);
20+
21+
public override FixAllProvider GetFixAllProvider()
22+
=> WellKnownFixAllProviders.BatchFixer;
23+
24+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
25+
{
26+
var document = context.Document;
27+
var diagnostic = context.Diagnostics[0];
28+
var root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
29+
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
30+
31+
if (node is not InvocationExpressionSyntax invocation ||
32+
await document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is not { } semanticModel ||
33+
semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemIOPath) is not { } pathType)
34+
{
35+
return;
36+
}
37+
38+
// Get the method name from diagnostic properties
39+
if (!diagnostic.Properties.TryGetValue(CollapseMultiplePathOperationsAnalyzer.MethodNameKey, out var methodName))
40+
{
41+
methodName = "Path";
42+
}
43+
44+
context.RegisterCodeFix(
45+
CodeAction.Create(
46+
string.Format(MicrosoftNetCoreAnalyzersResources.CollapseMultiplePathOperationsCodeFixTitle, methodName),
47+
createChangedDocument: cancellationToken => CollapsePathOperationAsync(document, root, invocation, pathType, semanticModel, cancellationToken),
48+
equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.CollapseMultiplePathOperationsCodeFixTitle)),
49+
diagnostic);
50+
}
51+
52+
private static Task<Document> CollapsePathOperationAsync(Document document, SyntaxNode root, InvocationExpressionSyntax invocation, INamedTypeSymbol pathType, SemanticModel semanticModel, CancellationToken cancellationToken)
53+
{
54+
// Collect all arguments by recursively unwrapping nested Path.Combine/Join calls
55+
var allArguments = CollectAllArguments(invocation, pathType, semanticModel);
56+
57+
// Create new argument list with all collected arguments
58+
var newArgumentList = SyntaxFactory.ArgumentList(
59+
SyntaxFactory.SeparatedList(allArguments));
60+
61+
// Create the new invocation with all arguments
62+
var newInvocation = invocation.WithArgumentList(newArgumentList)
63+
.WithTriviaFrom(invocation);
64+
65+
var newRoot = root.ReplaceNode(invocation, newInvocation);
66+
67+
return Task.FromResult(document.WithSyntaxRoot(newRoot));
68+
}
69+
70+
private static ArgumentSyntax[] CollectAllArguments(InvocationExpressionSyntax invocation, INamedTypeSymbol pathType, SemanticModel semanticModel)
71+
{
72+
var arguments = ImmutableArray.CreateBuilder<ArgumentSyntax>();
73+
74+
foreach (var argument in invocation.ArgumentList.Arguments)
75+
{
76+
if (argument.Expression is InvocationExpressionSyntax nestedInvocation &&
77+
IsPathCombineOrJoin(nestedInvocation, pathType, semanticModel, out var methodName) &&
78+
IsPathCombineOrJoin(invocation, pathType, semanticModel, out var outerMethodName) &&
79+
methodName == outerMethodName)
80+
{
81+
// Recursively collect arguments from nested invocation
82+
arguments.AddRange(CollectAllArguments(nestedInvocation, pathType, semanticModel));
83+
}
84+
else
85+
{
86+
arguments.Add(argument);
87+
}
88+
}
89+
90+
return arguments.ToArray();
91+
}
92+
93+
private static bool IsPathCombineOrJoin(InvocationExpressionSyntax invocation, INamedTypeSymbol pathType, SemanticModel semanticModel, out string methodName)
94+
{
95+
if (semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol methodSymbol &&
96+
SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, pathType) &&
97+
methodSymbol.Name is "Combine" or "Join")
98+
{
99+
methodName = methodSymbol.Name;
100+
return true;
101+
}
102+
103+
methodName = string.Empty;
104+
return false;
105+
}
106+
}
107+
}

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,6 +1908,30 @@ In many situations, logging is disabled or set to a log level that results in an
19081908
|CodeFix|True|
19091909
---
19101910

1911+
## [CA1876](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876): Do not use 'AsParallel' in 'foreach'
1912+
1913+
Using 'AsParallel()' directly in a 'foreach' loop has no effect. The 'foreach' statement iterates serially through the collection regardless. To parallelize LINQ operations, call 'AsParallel()' earlier in the query chain before other LINQ operators. To parallelize the loop itself, use 'Parallel.ForEach' instead.
1914+
1915+
|Item|Value|
1916+
|-|-|
1917+
|Category|Performance|
1918+
|Enabled|True|
1919+
|Severity|Info|
1920+
|CodeFix|False|
1921+
---
1922+
1923+
## [CA1877](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1877): Collapse consecutive Path.Combine or Path.Join operations
1924+
1925+
When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability.
1926+
1927+
|Item|Value|
1928+
|-|-|
1929+
|Category|Performance|
1930+
|Enabled|True|
1931+
|Severity|Info|
1932+
|CodeFix|True|
1933+
---
1934+
19111935
## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope
19121936

19131937
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.
@@ -2166,6 +2190,18 @@ JsonDocument implements IDisposable and needs to be properly disposed. When only
21662190
|CodeFix|True|
21672191
---
21682192

2193+
## [CA2027](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027): Cancel Task.Delay after Task.WhenAny completes
2194+
2195+
When Task.Delay is used with Task.WhenAny to implement a timeout, the timer created by Task.Delay continues to run even after WhenAny completes, wasting resources. If your target framework supports Task.WaitAsync, use that instead as it has built-in timeout support without leaving timers running. Otherwise, pass a CancellationToken to Task.Delay that can be canceled when the operation completes.
2196+
2197+
|Item|Value|
2198+
|-|-|
2199+
|Category|Reliability|
2200+
|Enabled|True|
2201+
|Severity|Info|
2202+
|CodeFix|False|
2203+
---
2204+
21692205
## [CA2100](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100): Review SQL queries for security vulnerabilities
21702206

21712207
SQL queries that directly use user input can be vulnerable to SQL injection attacks. Review this SQL query for potential vulnerabilities, and consider using a parameterized SQL query.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3507,6 +3507,46 @@
35073507
]
35083508
}
35093509
},
3510+
"CA1876": {
3511+
"id": "CA1876",
3512+
"shortDescription": "Do not use 'AsParallel' in 'foreach'",
3513+
"fullDescription": "Using 'AsParallel()' directly in a 'foreach' loop has no effect. The 'foreach' statement iterates serially through the collection regardless. To parallelize LINQ operations, call 'AsParallel()' earlier in the query chain before other LINQ operators. To parallelize the loop itself, use 'Parallel.ForEach' instead.",
3514+
"defaultLevel": "note",
3515+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876",
3516+
"properties": {
3517+
"category": "Performance",
3518+
"isEnabledByDefault": true,
3519+
"typeName": "DoNotUseAsParallelInForEachLoopAnalyzer",
3520+
"languages": [
3521+
"C#",
3522+
"Visual Basic"
3523+
],
3524+
"tags": [
3525+
"Telemetry",
3526+
"EnabledRuleInAggressiveMode"
3527+
]
3528+
}
3529+
},
3530+
"CA1877": {
3531+
"id": "CA1877",
3532+
"shortDescription": "Collapse consecutive Path.Combine or Path.Join operations",
3533+
"fullDescription": "When multiple Path.Combine or Path.Join operations are nested, they can be collapsed into a single operation for better performance and readability.",
3534+
"defaultLevel": "note",
3535+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1877",
3536+
"properties": {
3537+
"category": "Performance",
3538+
"isEnabledByDefault": true,
3539+
"typeName": "CollapseMultiplePathOperationsAnalyzer",
3540+
"languages": [
3541+
"C#",
3542+
"Visual Basic"
3543+
],
3544+
"tags": [
3545+
"Telemetry",
3546+
"EnabledRuleInAggressiveMode"
3547+
]
3548+
}
3549+
},
35103550
"CA2000": {
35113551
"id": "CA2000",
35123552
"shortDescription": "Dispose objects before losing scope",
@@ -3870,6 +3910,26 @@
38703910
]
38713911
}
38723912
},
3913+
"CA2027": {
3914+
"id": "CA2027",
3915+
"shortDescription": "Cancel Task.Delay after Task.WhenAny completes",
3916+
"fullDescription": "When Task.Delay is used with Task.WhenAny to implement a timeout, the timer created by Task.Delay continues to run even after WhenAny completes, wasting resources. If your target framework supports Task.WaitAsync, use that instead as it has built-in timeout support without leaving timers running. Otherwise, pass a CancellationToken to Task.Delay that can be canceled when the operation completes.",
3917+
"defaultLevel": "note",
3918+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027",
3919+
"properties": {
3920+
"category": "Reliability",
3921+
"isEnabledByDefault": true,
3922+
"typeName": "DoNotUseNonCancelableTaskDelayWithWhenAny",
3923+
"languages": [
3924+
"C#",
3925+
"Visual Basic"
3926+
],
3927+
"tags": [
3928+
"Telemetry",
3929+
"EnabledRuleInAggressiveMode"
3930+
]
3931+
}
3932+
},
38733933
"CA2100": {
38743934
"id": "CA2100",
38753935
"shortDescription": "Review SQL queries for security vulnerabilities",

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ Rule ID | Category | Severity | Notes
77
CA1873 | Performance | Info | AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
88
CA1874 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1874)
99
CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)
10+
CA1876 | Performance | Info | DoNotUseAsParallelInForEachLoopAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876)
11+
CA1877 | Performance | Info | CollapseMultiplePathOperationsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/cA1877)
1012
CA2023 | Reliability | Warning | LoggerMessageDefineAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2023)
1113
CA2024 | Reliability | Warning | DoNotUseEndOfStreamInAsyncMethods, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
1214
CA2025 | Reliability | Disabled | DoNotPassDisposablesIntoUnawaitedTasksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2025)
1315
CA2026 | Reliability | Info | PreferJsonElementParse, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026)
16+
CA2027 | Reliability | Info | DoNotUseNonCancelableTaskDelayWithWhenAny, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027)

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@
195195
<data name="DoNotUseCountWhenAnyCanBeUsedTitle" xml:space="preserve">
196196
<value>Do not use Count() or LongCount() when Any() can be used</value>
197197
</data>
198+
<data name="DoNotUseAsParallelInForEachLoopTitle" xml:space="preserve">
199+
<value>Do not use 'AsParallel' in 'foreach'</value>
200+
</data>
201+
<data name="DoNotUseAsParallelInForEachLoopMessage" xml:space="preserve">
202+
<value>Using 'AsParallel()' directly in a 'foreach' loop has no effect and the loop is not parallelized</value>
203+
</data>
204+
<data name="DoNotUseAsParallelInForEachLoopDescription" xml:space="preserve">
205+
<value>Using 'AsParallel()' directly in a 'foreach' loop has no effect. The 'foreach' statement iterates serially through the collection regardless. To parallelize LINQ operations, call 'AsParallel()' earlier in the query chain before other LINQ operators. To parallelize the loop itself, use 'Parallel.ForEach' instead.</value>
206+
</data>
198207
<data name="PreferConvertToHexStringOverBitConverterTitle" xml:space="preserve">
199208
<value>Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'</value>
200209
</data>
@@ -1637,6 +1646,15 @@
16371646
<data name="DoNotUseWhenAllWithSingleTaskFix" xml:space="preserve">
16381647
<value>Replace 'WhenAll' call with argument</value>
16391648
</data>
1649+
<data name="DoNotUseNonCancelableTaskDelayWithWhenAnyTitle" xml:space="preserve">
1650+
<value>Cancel Task.Delay after Task.WhenAny completes</value>
1651+
</data>
1652+
<data name="DoNotUseNonCancelableTaskDelayWithWhenAnyMessage" xml:space="preserve">
1653+
<value>Using Task.WhenAny with Task.Delay may result in a timer continuing to run after the operation completes, wasting resources</value>
1654+
</data>
1655+
<data name="DoNotUseNonCancelableTaskDelayWithWhenAnyDescription" xml:space="preserve">
1656+
<value>When Task.Delay is used with Task.WhenAny to implement a timeout, the timer created by Task.Delay continues to run even after WhenAny completes, wasting resources. If your target framework supports Task.WaitAsync, use that instead as it has built-in timeout support without leaving timers running. Otherwise, pass a CancellationToken to Task.Delay that can be canceled when the operation completes.</value>
1657+
</data>
16401658
<data name="UseStringEqualsOverStringCompareCodeFixTitle" xml:space="preserve">
16411659
<value>Use 'string.Equals'</value>
16421660
</data>
@@ -2231,4 +2249,16 @@ Widening and user defined conversions are not supported with generic types.</val
22312249
<data name="DoNotUseThreadVolatileReadWriteCodeFixTitle" xml:space="preserve">
22322250
<value>Replace obsolete call</value>
22332251
</data>
2252+
<data name="CollapseMultiplePathOperationsTitle" xml:space="preserve">
2253+
<value>Collapse consecutive Path.Combine or Path.Join operations</value>
2254+
</data>
2255+
<data name="CollapseMultiplePathOperationsMessage" xml:space="preserve">
2256+
<value>Multiple consecutive Path.{0} operations can be collapsed into a single operation</value>
2257+
</data>
2258+
<data name="CollapseMultiplePathOperationsDescription" xml:space="preserve">
2259+
<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>
2260+
</data>
2261+
<data name="CollapseMultiplePathOperationsCodeFixTitle" xml:space="preserve">
2262+
<value>Collapse into single Path.{0} operation</value>
2263+
</data>
22342264
</root>

0 commit comments

Comments
 (0)