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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0219](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0219.md)|Design|Set the language attribute in XML comment|👻|✔️|✔️|❌|
|[MA0220](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0220.md)|Design|The configured regular expression is not valid|⚠️|✔️|❌|❌|
|[MA0221](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0221.md)|Design|TryGetValue method should use \[MaybeNullWhen(false)\] on the value parameter|ℹ️|❌|✔️|❌|
|[MA0222](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0222.md)|Design|JsonSourceGenerationOptions should set RespectNullableAnnotations|⚠️|❌|✔️|❌|
|[MA0223](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0223.md)|Design|JsonSourceGenerationOptions should set RespectRequiredConstructorParameters|⚠️|❌|✔️|❌|

<!-- rules -->

Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@
|[MA0219](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0219.md)|Design|Set the language attribute in XML comment|<span title='Hidden'>👻</span>|✔️|✔️|❌|
|[MA0220](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0220.md)|Design|The configured regular expression is not valid|<span title='Warning'>⚠️</span>|✔️|❌|❌|
|[MA0221](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0221.md)|Design|TryGetValue method should use \[MaybeNullWhen(false)\] on the value parameter|<span title='Info'>ℹ️</span>|❌|✔️|❌|
|[MA0222](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0222.md)|Design|JsonSourceGenerationOptions should set RespectNullableAnnotations|<span title='Warning'>⚠️</span>|❌|✔️|❌|
|[MA0223](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0223.md)|Design|JsonSourceGenerationOptions should set RespectRequiredConstructorParameters|<span title='Warning'>⚠️</span>|❌|✔️|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down
57 changes: 57 additions & 0 deletions docs/Rules/MA0222.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# MA0222 - JsonSourceGenerationOptions should set RespectNullableAnnotations
<!-- sources -->
Sources: [JsonSourceGenerationOptionsAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/JsonSourceGenerationOptionsAnalyzer.cs), [JsonSourceGenerationOptionsFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/JsonSourceGenerationOptionsFixer.cs)
<!-- sources -->

`System.Text.Json` does not enforce the nullable annotations of the types it serializes and deserializes. `RespectNullableAnnotations`, introduced in .NET 9, makes the serializer throw when a `null` value is read into, or written from, a non-nullable property, field or constructor parameter. It defaults to `false` for backward compatibility, so a `JsonSerializerContext` that does not configure it silently produces instances whose non-nullable members are `null`.

This rule reports the `JsonSerializerContext` whose `[JsonSourceGenerationOptions]` attribute does not set the option, including the ones that have no `[JsonSourceGenerationOptions]` attribute at all. It does not require a particular value: setting the option to `false` is a deliberate choice, and satisfies the rule as much as setting it to `true`.

[MA0223](MA0223.md) reports the closely related `RespectRequiredConstructorParameters` option.

````csharp
using System.Text.Json;
using System.Text.Json.Serialization;

// ❌ The option is not configured, and deserializing {"Name":null} sets Name to null
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

// ✅ Deserializing {"Name":null} throws
[JsonSourceGenerationOptions(RespectNullableAnnotations = true)]
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

// ✅ The legacy behavior is kept, but the choice is explicit
[JsonSourceGenerationOptions(RespectNullableAnnotations = false)]
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

// ✅ JsonSerializerDefaults.Strict, introduced in .NET 10, sets the option
[JsonSourceGenerationOptions(JsonSerializerDefaults.Strict)]
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

class Person
{
public string Name { get; set; } = "";
}
````

Note that the option only rejects the `null` values that are present in the payload: a missing property is left to its default value, which is `null` for an uninitialized non-nullable reference type. Use the `required` modifier or `[JsonRequired]` to reject the payloads that do not contain the property.

The rule does not report anything when the target framework does not support the option.

The option can also be enabled for the whole application with the `System.Text.Json.Serialization.RespectNullableAnnotationsDefault` feature switch, which this rule does not detect:

````xml
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectNullableAnnotationsDefault" Value="true" />
</ItemGroup>
````

This rule is disabled by default as it requires every context to configure the option. To enable it, add the following to your `.editorconfig` file:

````editorconfig
dotnet_diagnostic.MA0222.severity = warning
````
52 changes: 52 additions & 0 deletions docs/Rules/MA0223.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# MA0223 - JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
<!-- sources -->
Sources: [JsonSourceGenerationOptionsAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/JsonSourceGenerationOptionsAnalyzer.cs), [JsonSourceGenerationOptionsFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/JsonSourceGenerationOptionsFixer.cs)
<!-- sources -->

For historical reasons, `System.Text.Json` treats all the constructor parameters as optional, and fills the ones that are missing from the payload with their default value. `RespectRequiredConstructorParameters`, introduced in .NET 9, makes the serializer throw when a non-optional constructor parameter is missing. It defaults to `false` for backward compatibility, so a `JsonSerializerContext` that does not configure it silently produces instances built from an incomplete payload.

This rule reports the `JsonSerializerContext` whose `[JsonSourceGenerationOptions]` attribute does not set the option, including the ones that have no `[JsonSourceGenerationOptions]` attribute at all. It does not require a particular value: setting the option to `false` is a deliberate choice, and satisfies the rule as much as setting it to `true`.

[MA0222](MA0222.md) reports the closely related `RespectNullableAnnotations` option.

````csharp
using System.Text.Json;
using System.Text.Json.Serialization;

// ❌ The option is not configured, and deserializing {} creates a Person whose Id is 0
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

// ✅ Deserializing {} throws as Id is missing
[JsonSourceGenerationOptions(RespectRequiredConstructorParameters = true)]
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

// ✅ The legacy behavior is kept, but the choice is explicit
[JsonSourceGenerationOptions(RespectRequiredConstructorParameters = false)]
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

// ✅ JsonSerializerDefaults.Strict, introduced in .NET 10, sets the option
[JsonSourceGenerationOptions(JsonSerializerDefaults.Strict)]
[JsonSerializable(typeof(Person))]
partial class PersonContext : JsonSerializerContext;

record Person(int Id);
````

The rule does not report anything when the target framework does not support the option.

The option can also be enabled for the whole application with the `System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault` feature switch, which this rule does not detect:

````xml
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault" Value="true" />
</ItemGroup>
````

This rule is disabled by default as it requires every context to configure the option. To enable it, add the following to your `.editorconfig` file:

````editorconfig
dotnet_diagnostic.MA0223.severity = warning
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Microsoft.CodeAnalysis.Simplification;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class JsonSourceGenerationOptionsFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.SetRespectNullableAnnotations, RuleIdentifiers.SetRespectRequiredConstructorParameters);

public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true);
if (nodeToFix is not (AttributeSyntax or TypeDeclarationSyntax))
return;

var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel is null)
return;

var attributeSymbol = semanticModel.Compilation.GetBestTypeByMetadataName("System.Text.Json.Serialization.JsonSourceGenerationOptionsAttribute");
if (attributeSymbol is null)
return;

foreach (var diagnostic in context.Diagnostics)
{
var propertyName = GetPropertyName(diagnostic.Id);
if (propertyName is null)
continue;

var title = $"Set {propertyName} to true";
context.RegisterCodeFix(
CodeAction.Create(title, ct => Refactor(context.Document, nodeToFix, propertyName, attributeSymbol, ct), equivalenceKey: title),
diagnostic);
}
}

private static string? GetPropertyName(string diagnosticId) => diagnosticId switch
{
RuleIdentifiers.SetRespectNullableAnnotations => "RespectNullableAnnotations",
RuleIdentifiers.SetRespectRequiredConstructorParameters => "RespectRequiredConstructorParameters",
_ => null,
};

private static async Task<Document> Refactor(Document document, SyntaxNode nodeToFix, string propertyName, INamedTypeSymbol attributeSymbol, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

if (nodeToFix is AttributeSyntax attribute)
{
var argument = SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals(propertyName), nameColon: null, SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression));
var argumentList = attribute.ArgumentList ?? SyntaxFactory.AttributeArgumentList();
editor.ReplaceNode(attribute, attribute.WithArgumentList(argumentList.AddArguments(argument)));
}
else
{
// The context has no [JsonSourceGenerationOptions] attribute, so the fix adds it
var generator = editor.Generator;
var newAttribute = generator.Attribute(
generator.TypeExpression(attributeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation),
[generator.AttributeArgument(propertyName, generator.TrueLiteralExpression())]);

editor.AddAttribute(nodeToFix, newAttribute);
}

return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,9 @@ dotnet_diagnostic.MA0220.severity = error

# MA0221: TryGetValue method should use [MaybeNullWhen(false)] on the value parameter
dotnet_diagnostic.MA0221.severity = error

# MA0222: JsonSourceGenerationOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0222.severity = error

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,9 @@ dotnet_diagnostic.MA0220.severity = suggestion

# MA0221: TryGetValue method should use [MaybeNullWhen(false)] on the value parameter
dotnet_diagnostic.MA0221.severity = suggestion

# MA0222: JsonSourceGenerationOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0222.severity = suggestion

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = suggestion
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,9 @@ dotnet_diagnostic.MA0220.severity = warning

# MA0221: TryGetValue method should use [MaybeNullWhen(false)] on the value parameter
dotnet_diagnostic.MA0221.severity = warning

# MA0222: JsonSourceGenerationOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0222.severity = warning

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = warning
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,9 @@ dotnet_diagnostic.MA0220.severity = warning

# MA0221: TryGetValue method should use [MaybeNullWhen(false)] on the value parameter
dotnet_diagnostic.MA0221.severity = none

# MA0222: JsonSourceGenerationOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0222.severity = none

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = none
6 changes: 6 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,9 @@ dotnet_diagnostic.MA0220.severity = none

# MA0221: TryGetValue method should use [MaybeNullWhen(false)] on the value parameter
dotnet_diagnostic.MA0221.severity = none

# MA0222: JsonSourceGenerationOptions should set RespectNullableAnnotations
dotnet_diagnostic.MA0222.severity = none

# MA0223: JsonSourceGenerationOptions should set RespectRequiredConstructorParameters
dotnet_diagnostic.MA0223.severity = none
2 changes: 2 additions & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ internal static class RuleIdentifiers
public const string MissingLanguageAttributeInXmlComment = "MA0219";
public const string InvalidRegexConfiguration = "MA0220";
public const string MissingMaybeNullWhenAttributeOnTryGetValue = "MA0221";
public const string SetRespectNullableAnnotations = "MA0222";
public const string SetRespectRequiredConstructorParameters = "MA0223";

public static string GetHelpUri(string identifier)
{
Expand Down
Loading