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
Expand Up @@ -2154,6 +2154,18 @@ Unawaited tasks that use 'IDisposable' instances may use those instances long af
|CodeFix|False|
---

## [CA2026](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026): Prefer JsonElement.Parse over JsonDocument.Parse().RootElement

JsonDocument implements IDisposable and needs to be properly disposed. When only the RootElement is needed, prefer JsonElement.Parse which doesn't require disposal.

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

## [CA2100](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100): Review SQL queries for security vulnerabilities

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3850,6 +3850,26 @@
]
}
},
"CA2026": {
"id": "CA2026",
"shortDescription": "Prefer JsonElement.Parse over JsonDocument.Parse().RootElement",
"fullDescription": "JsonDocument implements IDisposable and needs to be properly disposed. When only the RootElement is needed, prefer JsonElement.Parse which doesn't require disposal.",
"defaultLevel": "note",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026",
"properties": {
"category": "Reliability",
"isEnabledByDefault": true,
"typeName": "PreferJsonElementParse",
"languages": [
"C#",
"Visual Basic"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2100": {
"id": "CA2100",
"shortDescription": "Review SQL queries for security vulnerabilities",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.mic
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)
CA2026 | Reliability | Info | PreferJsonElementParse, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026)
Original file line number Diff line number Diff line change
Expand Up @@ -2054,6 +2054,18 @@ Widening and user defined conversions are not supported with generic types.</val
<data name="UseThrowHelperFix" xml:space="preserve">
<value>Use '{0}.{1}'</value>
</data>
<data name="PreferJsonElementParseTitle" xml:space="preserve">
<value>Prefer JsonElement.Parse over JsonDocument.Parse().RootElement</value>
</data>
<data name="PreferJsonElementParseMessage" xml:space="preserve">
<value>Use 'JsonElement.Parse' instead of 'JsonDocument.Parse(...).RootElement' to avoid resource leaks</value>
</data>
<data name="PreferJsonElementParseDescription" xml:space="preserve">
<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>
</data>
<data name="PreferJsonElementParseFix" xml:space="preserve">
<value>Use 'JsonElement.Parse'</value>
</data>
<data name="UseConcreteTypeDescription" xml:space="preserve">
<value>Using concrete types avoids virtual or interface call overhead and enables inlining.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 System.Linq;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
/// <summary>
/// Fixer for <see cref="PreferJsonElementParse"/>.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
public sealed class PreferJsonElementParseFixer : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(PreferJsonElementParse.RuleId);

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

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Document doc = context.Document;
SemanticModel model = await doc.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode node ||
model.GetOperation(node, context.CancellationToken) is not IPropertyReferenceOperation propertyReference ||
propertyReference.Property.Name != "RootElement" ||
propertyReference.Instance is not IInvocationOperation invocation ||
invocation.TargetMethod.Name != "Parse")
{
return;
}

string title = MicrosoftNetCoreAnalyzersResources.PreferJsonElementParseFix;
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: async ct =>
{
DocumentEditor editor = await DocumentEditor.CreateAsync(doc, ct).ConfigureAwait(false);
SyntaxGenerator generator = editor.Generator;

// Get the JsonElement type
INamedTypeSymbol? jsonElementType = model.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextJsonJsonElement);
if (jsonElementType == null)
{
return doc;
}

// Create the replacement: JsonElement.Parse(...)
// We need to use the same arguments that were passed to JsonDocument.Parse
var arguments = invocation.Arguments.Select(arg => arg.Syntax).ToArray();

SyntaxNode memberAccess = generator.MemberAccessExpression(
generator.TypeExpressionForStaticMemberAccess(jsonElementType),
"Parse");

SyntaxNode replacement = generator.InvocationExpression(memberAccess, arguments);

// Replace the entire property reference (JsonDocument.Parse(...).RootElement) with JsonElement.Parse(...)
editor.ReplaceNode(propertyReference.Syntax, replacement.WithTriviaFrom(propertyReference.Syntax));

return editor.GetChangedDocument();
},
equivalenceKey: title),
context.Diagnostics);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
using static MicrosoftNetCoreAnalyzersResources;

/// <summary>
/// CA2026: Prefer JsonElement.Parse over JsonDocument.Parse().RootElement
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class PreferJsonElementParse : DiagnosticAnalyzer
{
internal const string RuleId = "CA2026";

internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
RuleId,
CreateLocalizableResourceString(nameof(PreferJsonElementParseTitle)),
CreateLocalizableResourceString(nameof(PreferJsonElementParseMessage)),
DiagnosticCategory.Reliability,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(PreferJsonElementParseDescription)),
isPortedFxCopRule: false,
isDataflowRule: false);

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

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

context.RegisterCompilationStartAction(context =>
{
// Get the JsonDocument and JsonElement types
INamedTypeSymbol? jsonDocumentType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextJsonJsonDocument);
INamedTypeSymbol? jsonElementType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextJsonJsonElement);

if (jsonDocumentType is null || jsonElementType is null)
{
return;
}

// Get all JsonElement.Parse overloads for matching
var jsonElementParseOverloads = jsonElementType.GetMembers("Parse")
.OfType<IMethodSymbol>()
.Where(m => m.IsStatic)
.ToImmutableArray();

// Check if JsonElement.Parse exists
if (jsonElementParseOverloads.IsEmpty ||
!jsonDocumentType.GetMembers("Parse").Any(m => m is IMethodSymbol { IsStatic: true }))
{
return;
}

// Get the RootElement property
IPropertySymbol? rootElementProperty = jsonDocumentType.GetMembers("RootElement")
.OfType<IPropertySymbol>()
.FirstOrDefault();

if (rootElementProperty is null)
{
return;
}

context.RegisterOperationAction(context =>
{
var propertyReference = (IPropertyReferenceOperation)context.Operation;

// Check if this is accessing the RootElement property and the instance is a direct call to JsonDocument.Parse
if (!SymbolEqualityComparer.Default.Equals(propertyReference.Property, rootElementProperty) ||
propertyReference.Instance is not IInvocationOperation invocation ||
!SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, jsonDocumentType) ||
invocation.TargetMethod.Name != "Parse")
{
return;
}

// Now we have the pattern: JsonDocument.Parse(...).RootElement
// Check if there's a matching JsonElement.Parse overload with the same parameter types
var jsonDocumentParseMethod = invocation.TargetMethod;

foreach (var elementParseOverload in jsonElementParseOverloads)
{
if (elementParseOverload.Parameters.Length != jsonDocumentParseMethod.Parameters.Length)
{
continue;
}

bool parametersMatch = true;
for (int i = 0; i < elementParseOverload.Parameters.Length; i++)
{
if (!SymbolEqualityComparer.Default.Equals(elementParseOverload.Parameters[i].Type, jsonDocumentParseMethod.Parameters[i].Type))
{
parametersMatch = false;
break;
}
}

if (parametersMatch)
{
context.ReportDiagnostic(propertyReference.CreateDiagnostic(Rule));
break;
}
}
}, OperationKind.PropertyReference);
});
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading