Skip to content

Commit a4ffff6

Browse files
Copilotstephentoub
andauthored
Add CA2026 analyzer: Prefer JsonElement.Parse over JsonDocument.Parse().RootElement (#51209)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stephentoub <[email protected]> Co-authored-by: Stephen Toub <[email protected]>
1 parent 7eca15c commit a4ffff6

22 files changed

+1165
-1
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,6 +2154,18 @@ Unawaited tasks that use 'IDisposable' instances may use those instances long af
21542154
|CodeFix|False|
21552155
---
21562156

2157+
## [CA2026](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026): Prefer JsonElement.Parse over JsonDocument.Parse().RootElement
2158+
2159+
JsonDocument implements IDisposable and needs to be properly disposed. When only the RootElement is needed, prefer JsonElement.Parse which doesn't require disposal.
2160+
2161+
|Item|Value|
2162+
|-|-|
2163+
|Category|Reliability|
2164+
|Enabled|True|
2165+
|Severity|Info|
2166+
|CodeFix|True|
2167+
---
2168+
21572169
## [CA2100](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100): Review SQL queries for security vulnerabilities
21582170

21592171
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3850,6 +3850,26 @@
38503850
]
38513851
}
38523852
},
3853+
"CA2026": {
3854+
"id": "CA2026",
3855+
"shortDescription": "Prefer JsonElement.Parse over JsonDocument.Parse().RootElement",
3856+
"fullDescription": "JsonDocument implements IDisposable and needs to be properly disposed. When only the RootElement is needed, prefer JsonElement.Parse which doesn't require disposal.",
3857+
"defaultLevel": "note",
3858+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026",
3859+
"properties": {
3860+
"category": "Reliability",
3861+
"isEnabledByDefault": true,
3862+
"typeName": "PreferJsonElementParse",
3863+
"languages": [
3864+
"C#",
3865+
"Visual Basic"
3866+
],
3867+
"tags": [
3868+
"Telemetry",
3869+
"EnabledRuleInAggressiveMode"
3870+
]
3871+
}
3872+
},
38533873
"CA2100": {
38543874
"id": "CA2100",
38553875
"shortDescription": "Review SQL queries for security vulnerabilities",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.mic
1010
CA2023 | Reliability | Warning | LoggerMessageDefineAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2023)
1111
CA2024 | Reliability | Warning | DoNotUseEndOfStreamInAsyncMethods, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
1212
CA2025 | Reliability | Disabled | DoNotPassDisposablesIntoUnawaitedTasksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2025)
13+
CA2026 | Reliability | Info | PreferJsonElementParse, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026)

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2054,6 +2054,18 @@ Widening and user defined conversions are not supported with generic types.</val
20542054
<data name="UseThrowHelperFix" xml:space="preserve">
20552055
<value>Use '{0}.{1}'</value>
20562056
</data>
2057+
<data name="PreferJsonElementParseTitle" xml:space="preserve">
2058+
<value>Prefer JsonElement.Parse over JsonDocument.Parse().RootElement</value>
2059+
</data>
2060+
<data name="PreferJsonElementParseMessage" xml:space="preserve">
2061+
<value>Use 'JsonElement.Parse' instead of 'JsonDocument.Parse(...).RootElement' to avoid resource leaks</value>
2062+
</data>
2063+
<data name="PreferJsonElementParseDescription" xml:space="preserve">
2064+
<value>JsonDocument implements IDisposable and needs to be properly disposed. When only the RootElement is needed, prefer JsonElement.Parse which doesn't require disposal.</value>
2065+
</data>
2066+
<data name="PreferJsonElementParseFix" xml:space="preserve">
2067+
<value>Use 'JsonElement.Parse'</value>
2068+
</data>
20572069
<data name="UseConcreteTypeDescription" xml:space="preserve">
20582070
<value>Using concrete types avoids virtual or interface call overhead and enables inlining.</value>
20592071
</data>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 System.Linq;
6+
using System.Threading.Tasks;
7+
using Analyzer.Utilities;
8+
using Analyzer.Utilities.Extensions;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.CodeFixes;
12+
using Microsoft.CodeAnalysis.Editing;
13+
using Microsoft.CodeAnalysis.Operations;
14+
15+
namespace Microsoft.NetCore.Analyzers.Runtime
16+
{
17+
/// <summary>
18+
/// Fixer for <see cref="PreferJsonElementParse"/>.
19+
/// </summary>
20+
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
21+
public sealed class PreferJsonElementParseFixer : CodeFixProvider
22+
{
23+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
24+
ImmutableArray.Create(PreferJsonElementParse.RuleId);
25+
26+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
27+
28+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
29+
{
30+
Document doc = context.Document;
31+
SemanticModel model = await doc.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
32+
SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
33+
34+
if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode node ||
35+
model.GetOperation(node, context.CancellationToken) is not IPropertyReferenceOperation propertyReference ||
36+
propertyReference.Property.Name != "RootElement" ||
37+
propertyReference.Instance is not IInvocationOperation invocation ||
38+
invocation.TargetMethod.Name != "Parse")
39+
{
40+
return;
41+
}
42+
43+
string title = MicrosoftNetCoreAnalyzersResources.PreferJsonElementParseFix;
44+
context.RegisterCodeFix(
45+
CodeAction.Create(
46+
title,
47+
createChangedDocument: async ct =>
48+
{
49+
DocumentEditor editor = await DocumentEditor.CreateAsync(doc, ct).ConfigureAwait(false);
50+
SyntaxGenerator generator = editor.Generator;
51+
52+
// Get the JsonElement type
53+
INamedTypeSymbol? jsonElementType = model.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextJsonJsonElement);
54+
if (jsonElementType == null)
55+
{
56+
return doc;
57+
}
58+
59+
// Create the replacement: JsonElement.Parse(...)
60+
// We need to use the same arguments that were passed to JsonDocument.Parse
61+
var arguments = invocation.Arguments.Select(arg => arg.Syntax).ToArray();
62+
63+
SyntaxNode memberAccess = generator.MemberAccessExpression(
64+
generator.TypeExpressionForStaticMemberAccess(jsonElementType),
65+
"Parse");
66+
67+
SyntaxNode replacement = generator.InvocationExpression(memberAccess, arguments);
68+
69+
// Replace the entire property reference (JsonDocument.Parse(...).RootElement) with JsonElement.Parse(...)
70+
editor.ReplaceNode(propertyReference.Syntax, replacement.WithTriviaFrom(propertyReference.Syntax));
71+
72+
return editor.GetChangedDocument();
73+
},
74+
equivalenceKey: title),
75+
context.Diagnostics);
76+
}
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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.Linq;
5+
using Analyzer.Utilities;
6+
using Analyzer.Utilities.Extensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using Microsoft.CodeAnalysis.Operations;
10+
11+
namespace Microsoft.NetCore.Analyzers.Runtime
12+
{
13+
using static MicrosoftNetCoreAnalyzersResources;
14+
15+
/// <summary>
16+
/// CA2026: Prefer JsonElement.Parse over JsonDocument.Parse().RootElement
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
19+
public sealed class PreferJsonElementParse : DiagnosticAnalyzer
20+
{
21+
internal const string RuleId = "CA2026";
22+
23+
internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
24+
RuleId,
25+
CreateLocalizableResourceString(nameof(PreferJsonElementParseTitle)),
26+
CreateLocalizableResourceString(nameof(PreferJsonElementParseMessage)),
27+
DiagnosticCategory.Reliability,
28+
RuleLevel.IdeSuggestion,
29+
CreateLocalizableResourceString(nameof(PreferJsonElementParseDescription)),
30+
isPortedFxCopRule: false,
31+
isDataflowRule: false);
32+
33+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);
34+
35+
public override void Initialize(AnalysisContext context)
36+
{
37+
context.EnableConcurrentExecution();
38+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
39+
40+
context.RegisterCompilationStartAction(context =>
41+
{
42+
// Get the JsonDocument and JsonElement types
43+
INamedTypeSymbol? jsonDocumentType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextJsonJsonDocument);
44+
INamedTypeSymbol? jsonElementType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextJsonJsonElement);
45+
46+
if (jsonDocumentType is null || jsonElementType is null)
47+
{
48+
return;
49+
}
50+
51+
// Get all JsonElement.Parse overloads for matching
52+
var jsonElementParseOverloads = jsonElementType.GetMembers("Parse")
53+
.OfType<IMethodSymbol>()
54+
.Where(m => m.IsStatic)
55+
.ToImmutableArray();
56+
57+
// Check if JsonElement.Parse exists
58+
if (jsonElementParseOverloads.IsEmpty ||
59+
!jsonDocumentType.GetMembers("Parse").Any(m => m is IMethodSymbol { IsStatic: true }))
60+
{
61+
return;
62+
}
63+
64+
// Get the RootElement property
65+
IPropertySymbol? rootElementProperty = jsonDocumentType.GetMembers("RootElement")
66+
.OfType<IPropertySymbol>()
67+
.FirstOrDefault();
68+
69+
if (rootElementProperty is null)
70+
{
71+
return;
72+
}
73+
74+
context.RegisterOperationAction(context =>
75+
{
76+
var propertyReference = (IPropertyReferenceOperation)context.Operation;
77+
78+
// Check if this is accessing the RootElement property and the instance is a direct call to JsonDocument.Parse
79+
if (!SymbolEqualityComparer.Default.Equals(propertyReference.Property, rootElementProperty) ||
80+
propertyReference.Instance is not IInvocationOperation invocation ||
81+
!SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, jsonDocumentType) ||
82+
invocation.TargetMethod.Name != "Parse")
83+
{
84+
return;
85+
}
86+
87+
// Now we have the pattern: JsonDocument.Parse(...).RootElement
88+
// Check if there's a matching JsonElement.Parse overload with the same parameter types
89+
var jsonDocumentParseMethod = invocation.TargetMethod;
90+
91+
foreach (var elementParseOverload in jsonElementParseOverloads)
92+
{
93+
if (elementParseOverload.Parameters.Length != jsonDocumentParseMethod.Parameters.Length)
94+
{
95+
continue;
96+
}
97+
98+
bool parametersMatch = true;
99+
for (int i = 0; i < elementParseOverload.Parameters.Length; i++)
100+
{
101+
if (!SymbolEqualityComparer.Default.Equals(elementParseOverload.Parameters[i].Type, jsonDocumentParseMethod.Parameters[i].Type))
102+
{
103+
parametersMatch = false;
104+
break;
105+
}
106+
}
107+
108+
if (parametersMatch)
109+
{
110+
context.ReportDiagnostic(propertyReference.CreateDiagnostic(Rule));
111+
break;
112+
}
113+
}
114+
}, OperationKind.PropertyReference);
115+
});
116+
}
117+
}
118+
}

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)